GeoRuby 1.1.2 → 1.2.0

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.
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