GeoRuby 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/README +16 -13
  2. data/lib/geo_ruby/shp4r/dbf.rb +234 -0
  3. data/lib/geo_ruby/shp4r/shp.rb +301 -0
  4. data/lib/geo_ruby/simple_features/geometry.rb +1 -0
  5. data/lib/geo_ruby.rb +1 -1
  6. data/rakefile.rb +4 -4
  7. data/test/data/point.dbf +0 -0
  8. data/test/data/point.shp +0 -0
  9. data/test/data/point.shx +0 -0
  10. data/test/data/polygon.dbf +0 -0
  11. data/test/data/polygon.shp +0 -0
  12. data/test/data/polygon.shx +0 -0
  13. data/test/data/polyline.dbf +0 -0
  14. data/test/data/polyline.shp +0 -0
  15. data/test/data/polyline.shx +0 -0
  16. data/test/test_shp.rb +77 -0
  17. data/tools/db.yml +6 -0
  18. data/tools/lib/spatial_adapter/MIT-LICENSE +7 -0
  19. data/tools/lib/spatial_adapter/README +116 -0
  20. data/tools/lib/spatial_adapter/init.rb +10 -0
  21. data/tools/lib/spatial_adapter/lib/common_spatial_adapter.rb +175 -0
  22. data/tools/lib/spatial_adapter/lib/mysql_spatial_adapter.rb +143 -0
  23. data/tools/lib/spatial_adapter/lib/post_gis_adapter.rb +333 -0
  24. data/tools/lib/spatial_adapter/rakefile.rb +35 -0
  25. data/tools/lib/spatial_adapter/test/access_mysql_test.rb +87 -0
  26. data/tools/lib/spatial_adapter/test/access_postgis_test.rb +151 -0
  27. data/tools/lib/spatial_adapter/test/common/common_mysql.rb +18 -0
  28. data/tools/lib/spatial_adapter/test/common/common_postgis.rb +19 -0
  29. data/tools/lib/spatial_adapter/test/db/database_mysql.yml +5 -0
  30. data/tools/lib/spatial_adapter/test/db/database_postgis.yml +4 -0
  31. data/tools/lib/spatial_adapter/test/find_mysql_test.rb +64 -0
  32. data/tools/lib/spatial_adapter/test/find_postgis_test.rb +65 -0
  33. data/tools/lib/spatial_adapter/test/migration_mysql_test.rb +136 -0
  34. data/tools/lib/spatial_adapter/test/migration_postgis_test.rb +170 -0
  35. data/tools/lib/spatial_adapter/test/models/models_mysql.rb +25 -0
  36. data/tools/lib/spatial_adapter/test/models/models_postgis.rb +41 -0
  37. data/tools/lib/spatial_adapter/test/schema/schema_mysql.rb +40 -0
  38. data/tools/lib/spatial_adapter/test/schema/schema_postgis.rb +69 -0
  39. data/tools/shp2sql.rb +91 -0
  40. metadata +47 -4
@@ -0,0 +1,143 @@
1
+ require 'active_record'
2
+ require 'geo_ruby'
3
+ require 'common_spatial_adapter'
4
+
5
+ include GeoRuby::SimpleFeatures
6
+
7
+
8
+ #add a method to_fixture_format 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)
9
+ GeoRuby::SimpleFeatures::Geometry.class_eval do
10
+ def to_fixture_format
11
+ "!binary | #{[(255.chr * 4) + as_wkb].pack('m')}"
12
+ end
13
+ end
14
+
15
+
16
+ ActiveRecord::Base.class_eval do
17
+ #Redefinition of the method to do something special when a geometric column is encountered
18
+ def self.construct_conditions_from_arguments(attribute_names, arguments)
19
+ conditions = []
20
+ attribute_names.each_with_index do |name, idx|
21
+ if columns_hash[name].is_a?(SpatialColumn)
22
+ #when the discriminating column is spatial, always use the MBRIntersects (bounding box intersection check) operator : the user can pass either a geometric object (which will be transformed to a string using the quote method of the database adapter) or an array with the corner points of a bounding box
23
+ if arguments[idx].is_a?(Array)
24
+ conditions << "MBRIntersects(?, #{table_name}.#{connection.quote_column_name(name)}) "
25
+ #using some georuby utility : The multipoint has a bbox whose corners are the 2 points passed as parameters : [ pt1, pt2]
26
+ arguments[idx]= MultiPoint.from_coordinates(arguments[idx])
27
+ else
28
+ conditions << "MBRIntersects(?, #{table_name}.#{connection.quote_column_name(name)}) "
29
+ end
30
+ else
31
+ conditions << "#{table_name}.#{connection.quote_column_name(name)} #{attribute_condition(arguments[idx])} "
32
+ end
33
+ end
34
+ [ conditions.join(" AND "), *arguments[0...attribute_names.length] ]
35
+ end
36
+ end
37
+
38
+
39
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
40
+
41
+ include SpatialAdapter
42
+
43
+ alias :original_native_database_types :native_database_types
44
+ def native_database_types
45
+ original_native_database_types.merge!(geometry_data_types)
46
+ end
47
+
48
+ alias :original_quote :quote
49
+ #Redefines the quote method to add behaviour for when a Geometry is encountered ; used when binding variables in find_by methods
50
+ def quote(value, column = nil)
51
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
52
+ "GeomFromWKB(0x#{value.as_hex_wkb},#{value.srid})"
53
+ else
54
+ original_quote(value,column)
55
+ end
56
+ end
57
+
58
+ #Redefinition of columns to add the information that a column is geometric
59
+ def columns(table_name, name = nil)#:nodoc:
60
+ sql = "SHOW FIELDS FROM #{table_name}"
61
+ columns = []
62
+ execute(sql, name).each do |field|
63
+ if field[1] =~ /geometry|point|linestring|polygon|multipoint|multilinestring|multipolygon|geometrycollection/i
64
+ #to note that the column is spatial
65
+ columns << ActiveRecord::ConnectionAdapters::SpatialMysqlColumn.new(field[0], field[4], field[1], field[2] == "YES")
66
+ else
67
+ columns << ActiveRecord::ConnectionAdapters::MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES")
68
+ end
69
+ end
70
+ columns
71
+ end
72
+
73
+
74
+ #operations relative to migrations
75
+
76
+ #Redefines add_index to support the case where the index is spatial
77
+ #If the :spatial key in the options table is true, then the sql string for a spatial index is created
78
+ def add_index(table_name,column_name,options = {})
79
+ index_name = options[:name] || "#{table_name}_#{Array(column_name).first}_index"
80
+
81
+ if options[:spatial]
82
+ if column_name.is_a?(Array) and column_name.length > 1
83
+ #one by one or error : Should raise exception instead? ; use default name even if name passed as argument
84
+ Array(column_name).each do |col|
85
+ execute "CREATE SPATIAL INDEX #{table_name}_#{col}_index ON #{table_name} (#{col})"
86
+ end
87
+ else
88
+ col = Array(column_name)[0]
89
+ execute "CREATE SPATIAL INDEX #{index_name} ON #{table_name} (#{col})"
90
+ end
91
+ else
92
+ index_type = options[:unique] ? "UNIQUE" : ""
93
+ #all together
94
+ execute "CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
95
+ end
96
+ end
97
+
98
+ #Check the nature of the index : If it is SPATIAL, it is indicated in the IndexDefinition object (redefined to add the spatial flag in spatial_adapter_common.rb)
99
+ def indexes(table_name, name = nil)#:nodoc:
100
+ indexes = []
101
+ current_index = nil
102
+ execute("SHOW KEYS FROM #{table_name}", name).each do |row|
103
+ if current_index != row[2]
104
+ next if row[2] == "PRIMARY" # skip the primary key
105
+ current_index = row[2]
106
+ indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.new(row[0], row[2], row[1] == "0", row[10] == "SPATIAL",[])
107
+ end
108
+ indexes.last.columns << row[4]
109
+ end
110
+ indexes
111
+ end
112
+
113
+ #Get the table creation options : Only the engine for now. The text encoding could also be parsed and returned here.
114
+ def options_for(table)
115
+ result = execute("show table status like '#{table}'")
116
+ engine = result.fetch_row[1]
117
+ if engine !~ /inno/i #inno is default so do nothing for it in order not to clutter the migration
118
+ "ENGINE=#{engine}"
119
+ else
120
+ nil
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+ module ActiveRecord
127
+ module ConnectionAdapters
128
+ class SpatialMysqlColumn < MysqlColumn
129
+
130
+ include SpatialColumn
131
+
132
+ #MySql-specific geometry string parsing. By default, MySql returns geometries in strict wkb format with "0" characters in the first 4 positions.
133
+ def self.string_to_geometry(string)
134
+ return string unless string.is_a?(String)
135
+ begin
136
+ GeoRuby::SimpleFeatures::Geometry.from_ewkb(string[4..-1])
137
+ rescue Exception => exception
138
+ nil
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,333 @@
1
+ require 'active_record'
2
+ require 'geo_ruby'
3
+ require 'common_spatial_adapter'
4
+
5
+ include GeoRuby::SimpleFeatures
6
+
7
+ #tables to ignore in migration : relative to PostGIS management of geometric columns
8
+ ActiveRecord::SchemaDumper.ignore_tables << "spatial_ref_sys" << "geometry_columns"
9
+
10
+
11
+ #add a method to_fixture_format 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)
12
+ GeoRuby::SimpleFeatures::Geometry.class_eval do
13
+ def to_fixture_format
14
+ as_hex_ewkb
15
+ end
16
+ end
17
+
18
+
19
+ ActiveRecord::Base.class_eval do
20
+ def self.construct_conditions_from_arguments(attribute_names, arguments)
21
+ conditions = []
22
+ attribute_names.each_with_index do |name, idx|
23
+ if columns_hash[name].is_a?(SpatialColumn)
24
+ #when the discriminating column is spatial, always use the && (bounding box intersection check) operator : the user can pass either a geometric object (which will be transformed to a string using the quote method of the database adapter) or an array representing 2 opposite corners of a bounding box
25
+ if arguments[idx].is_a?(Array)
26
+ bbox = arguments[idx]
27
+ conditions << "#{table_name}.#{connection.quote_column_name(name)} && SetSRID(?::box3d, #{bbox[2] || -1} ) "
28
+ #Could do without the ? and replace directly with the quoted BBOX3D but like this, the flow is the same everytime
29
+ arguments[idx]= "BOX3D(" + bbox[0].join(" ") + "," + bbox[1].join(" ") + ")"
30
+ else
31
+ conditions << "#{table_name}.#{connection.quote_column_name(name)} && ? "
32
+ end
33
+ else
34
+ conditions << "#{table_name}.#{connection.quote_column_name(name)} #{attribute_condition(arguments[idx])} "
35
+ end
36
+ end
37
+ [ conditions.join(" AND "), *arguments[0...attribute_names.length] ]
38
+ end
39
+ end
40
+
41
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
42
+
43
+ include SpatialAdapter
44
+
45
+ alias :original_native_database_types :native_database_types
46
+ def native_database_types
47
+ original_native_database_types.merge!(geometry_data_types)
48
+ end
49
+
50
+ alias :original_quote :quote
51
+ #Redefines the quote method to add behaviour for when a Geometry is encountered
52
+ def quote(value, column = nil)
53
+ if value.kind_of?(GeoRuby::SimpleFeatures::Geometry)
54
+ "'#{value.as_hex_ewkb}'"
55
+ else
56
+ original_quote(value,column)
57
+ end
58
+ end
59
+
60
+ def create_table(name, options = {})
61
+ table_definition = ActiveRecord::ConnectionAdapters::PostgreSQLTableDefinition.new(self)
62
+ table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
63
+
64
+ yield table_definition
65
+
66
+ if options[:force]
67
+ drop_table(name) rescue nil
68
+ end
69
+
70
+ create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
71
+ create_sql << "#{name} ("
72
+ create_sql << table_definition.to_sql
73
+ create_sql << ") #{options[:options]}"
74
+ execute create_sql
75
+
76
+ #added to create the geometric columns identified during the table definition
77
+ unless table_definition.geom_columns.nil?
78
+ table_definition.geom_columns.each do |geom_column|
79
+ execute geom_column.to_sql(name)
80
+ end
81
+ end
82
+ end
83
+
84
+ alias :original_remove_column :remove_column
85
+ def remove_column(table_name,column_name)
86
+ columns(table_name).each do |col|
87
+ if col.name.to_s == column_name.to_s
88
+ #check if the column is geometric
89
+ unless geometry_data_types[col.type].nil?
90
+ execute "SELECT DropGeometryColumn('#{table_name}','#{column_name}')"
91
+ else
92
+ original_remove_column(table_name,column_name)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ alias :original_add_column :add_column
99
+ def add_column(table_name, column_name, type, options = {})
100
+ unless geometry_data_types[type].nil?
101
+ geom_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumnDefinition.new(self,column_name, type, nil,nil,options[:null],options[:srid] || -1 , options[:with_z] || false , options[:with_m] || false)
102
+ execute geom_column.to_sql(table_name)
103
+ else
104
+ original_add_column(table_name,column_name,type,options)
105
+ end
106
+ end
107
+
108
+
109
+
110
+ #Adds a GIST spatial index to a column. Its name will be <table_name>_<column_name>_spatial_index unless the key :name is present in the options hash, in which case its value is taken as the name of the index.
111
+ def add_index(table_name,column_name,options = {})
112
+ index_name = options[:name] ||"#{table_name}_#{Array(column_name).first}_index"
113
+ if options[:spatial]
114
+ if column_name.is_a?(Array) and column_name.length > 1
115
+ #one by one or error : Should raise exception instead? ; use default name even if name passed as argument
116
+ Array(column_name).each do |col|
117
+ execute "CREATE INDEX #{table_name}_#{col}_index ON #{table_name} USING GIST (#{col} GIST_GEOMETRY_OPS)"
118
+ end
119
+ else
120
+ col = Array(column_name)[0]
121
+ execute "CREATE INDEX #{index_name} ON #{table_name} USING GIST (#{col} GIST_GEOMETRY_OPS)"
122
+ end
123
+ else
124
+ index_type = options[:unique] ? "UNIQUE" : ""
125
+ #all together
126
+ execute "CREATE #{index_type} INDEX #{index_name} ON #{table_name} (#{Array(column_name).join(", ")})"
127
+ end
128
+ end
129
+
130
+
131
+ def indexes(table_name, name = nil) #:nodoc:
132
+ result = query(<<-SQL, name)
133
+ SELECT i.relname, d.indisunique, a.attname , am.amname
134
+ FROM pg_class t, pg_class i, pg_index d, pg_attribute a, pg_am am
135
+ WHERE i.relkind = 'i'
136
+ AND d.indexrelid = i.oid
137
+ AND d.indisprimary = 'f'
138
+ AND t.oid = d.indrelid
139
+ AND i.relam = am.oid
140
+ AND t.relname = '#{table_name}'
141
+ AND a.attrelid = t.oid
142
+ AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
143
+ OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
144
+ OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
145
+ OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
146
+ OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
147
+ ORDER BY i.relname
148
+ SQL
149
+
150
+ current_index = nil
151
+ indexes = []
152
+
153
+ result.each do |row|
154
+ if current_index != row[0]
155
+ indexes << ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, row[0], row[1] == "t", row[3] == "gist" ,[]) #index type gist indicates a spatial index (probably not totally true but let's simplify!)
156
+ current_index = row[0]
157
+ end
158
+
159
+ indexes.last.columns << row[2]
160
+ end
161
+
162
+ indexes
163
+ end
164
+
165
+ def columns(table_name, name = nil) #:nodoc:
166
+ raw_geom_infos = column_spatial_info(table_name)
167
+
168
+ column_definitions(table_name).collect do |name, type, default, notnull|
169
+ if type =~ /geometry/i and raw_geom_infos[name]
170
+ raw_geom_info = raw_geom_infos[name]
171
+
172
+ ActiveRecord::ConnectionAdapters::SpatialPostgreSQLColumn.new(name,default_value(default),raw_geom_info.type,notnull == "f",raw_geom_info.srid,raw_geom_info.with_z,raw_geom_info.with_m)
173
+ else
174
+ ActiveRecord::ConnectionAdapters::Column.new(name, default_value(default), translate_field_type(type),notnull == "f")
175
+ end
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def column_spatial_info(table_name)
182
+ constr = query <<-end_sql
183
+ SELECT pg_get_constraintdef(oid)
184
+ FROM pg_constraint
185
+ WHERE conrelid = '#{table_name}'::regclass
186
+ AND contype = 'c'
187
+ end_sql
188
+
189
+ raw_geom_infos = {}
190
+ constr.each do |constr_def_a|
191
+ constr_def = constr_def_a[0] #only 1 column in the result
192
+ if constr_def =~ /geometrytype\(["']?([^"')]+)["']?\)\s*=\s*'([^']+)'/i
193
+ column_name,type = $1,$2
194
+ if type[-1] == ?M
195
+ with_m = true
196
+ type.chop!
197
+ else
198
+ with_m = false
199
+ end
200
+ raw_geom_info = raw_geom_infos[column_name] || ActiveRecord::ConnectionAdapters::RawGeomInfo.new
201
+ raw_geom_info.type = type
202
+ raw_geom_info.with_m = with_m
203
+ raw_geom_infos[column_name] = raw_geom_info
204
+ elsif constr_def =~ /ndims\(["']?([^"')]+)["']?\)\s*=\s*(\d+)/i
205
+ column_name,dimension = $1,$2
206
+ raw_geom_info = raw_geom_infos[column_name] || ActiveRecord::ConnectionAdapters::RawGeomInfo.new
207
+ raw_geom_info.dimension = dimension.to_i
208
+ raw_geom_infos[column_name] = raw_geom_info
209
+ elsif constr_def =~ /srid\(["']?([^"')]+)["']?\)\s*=\s*(-?\d+)/i
210
+ column_name,srid = $1,$2
211
+ raw_geom_info = raw_geom_infos[column_name] || ActiveRecord::ConnectionAdapters::RawGeomInfo.new
212
+ raw_geom_info.srid = srid.to_i
213
+ raw_geom_infos[column_name] = raw_geom_info
214
+ end #if constr_def
215
+ end #constr.each
216
+
217
+ raw_geom_infos.each_value do |raw_geom_info|
218
+ #check the presence of z and m
219
+ raw_geom_info.convert!
220
+ end
221
+
222
+ raw_geom_infos
223
+
224
+ end
225
+
226
+ end
227
+
228
+ module ActiveRecord
229
+ module ConnectionAdapters
230
+ class RawGeomInfo < Struct.new(:type,:srid,:dimension,:with_z,:with_m) #:nodoc:
231
+ def convert!
232
+ self.type = "geometry" if self.type.nil? #if geometry the geometrytype constraint is not present : need to set the type here then
233
+
234
+ if dimension == 4
235
+ self.with_m = true
236
+ self.with_z = true
237
+ elsif dimension == 3
238
+ if with_m
239
+ self.with_z = false
240
+ self.with_m = true
241
+ else
242
+ self.with_z = true
243
+ self.with_m = false
244
+ end
245
+ else
246
+ self.with_z = false
247
+ self.with_m = false
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+
255
+ module ActiveRecord
256
+ module ConnectionAdapters
257
+ class PostgreSQLTableDefinition < TableDefinition
258
+ attr_reader :geom_columns
259
+
260
+ def column(name, type, options = {})
261
+ unless @base.geometry_data_types[type].nil?
262
+ geom_column = PostgreSQLColumnDefinition.new(@base,name, type)
263
+ geom_column.null = options[:null]
264
+ geom_column.srid = options[:srid] || -1
265
+ geom_column.with_z = options[:with_z] || false
266
+ geom_column.with_m = options[:with_m] || false
267
+
268
+ @geom_columns = [] if @geom_columns.nil?
269
+ @geom_columns << geom_column
270
+ else
271
+ super(name,type,options)
272
+ end
273
+ end
274
+ end
275
+
276
+ class PostgreSQLColumnDefinition < ColumnDefinition
277
+ attr_accessor :srid, :with_z,:with_m
278
+ attr_reader :spatial
279
+
280
+ def initialize(base = nil, name = nil, type=nil, limit=nil, default=nil,null=nil,srid=-1,with_z=false,with_m=false)
281
+ super(base, name, type, limit, default,null)
282
+ @spatial=true
283
+ @srid=srid
284
+ @with_z=with_z
285
+ @with_m=with_m
286
+ end
287
+
288
+ def to_sql(table_name)
289
+ if @spatial
290
+ type_sql = type_to_sql(type.to_sym)
291
+ type_sql += "M" if with_m and !with_z
292
+ if with_m and with_z
293
+ dimension = 4
294
+ elsif with_m or with_z
295
+ dimension = 3
296
+ else
297
+ dimension = 2
298
+ end
299
+
300
+ column_sql = "SELECT AddGeometryColumn('#{table_name}','#{name}',#{srid},'#{type_sql}',#{dimension})"
301
+ column_sql += ";ALTER TABLE #{table_name} ALTER #{name} SET NOT NULL" if null == false
302
+ column_sql
303
+ else
304
+ super
305
+ end
306
+ end
307
+
308
+
309
+ private
310
+ def type_to_sql(name, limit=nil)
311
+ base.type_to_sql(name, limit) rescue name
312
+ end
313
+
314
+ end
315
+
316
+ end
317
+ end
318
+
319
+ #Would prefer creation of a PostgreSQLColumn type instead but I would need to reimplement methods where Column objects are instantiated so I leave it like this
320
+ module ActiveRecord
321
+ module ConnectionAdapters
322
+ class SpatialPostgreSQLColumn < Column
323
+
324
+ include SpatialColumn
325
+
326
+ #Transforms a string to a geometry. PostGIS returns a HewEWKB string.
327
+ def self.string_to_geometry(string)
328
+ return string unless string.is_a?(String)
329
+ GeoRuby::SimpleFeatures::Geometry.from_hex_ewkb(string) rescue nil
330
+ end
331
+ end
332
+ end
333
+ end