rgeo 0.1.20 → 0.1.21

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 (55) hide show
  1. data/History.rdoc +10 -0
  2. data/README.rdoc +38 -35
  3. data/Version +1 -1
  4. data/lib/active_record/connection_adapters/mysql2spatial_adapter.rb +1 -3
  5. data/lib/active_record/connection_adapters/mysqlspatial_adapter.rb +4 -4
  6. data/lib/active_record/connection_adapters/postgis_adapter.rb +426 -0
  7. data/lib/active_record/connection_adapters/spatialite_adapter.rb +488 -0
  8. data/lib/rgeo.rb +10 -29
  9. data/lib/rgeo/active_record/arel_modifications.rb +1 -0
  10. data/lib/rgeo/active_record/base_modifications.rb +27 -10
  11. data/lib/rgeo/active_record/common.rb +128 -0
  12. data/lib/rgeo/active_record/mysql_common.rb +14 -51
  13. data/lib/rgeo/cartesian/factory.rb +2 -2
  14. data/lib/rgeo/coord_sys.rb +1 -1
  15. data/lib/rgeo/coord_sys/proj4.rb +3 -2
  16. data/lib/rgeo/error.rb +0 -3
  17. data/lib/rgeo/feature.rb +1 -3
  18. data/lib/rgeo/feature/factory_generator.rb +8 -0
  19. data/lib/rgeo/geography/factory.rb +2 -2
  20. data/lib/rgeo/geography/interface.rb +3 -3
  21. data/lib/rgeo/geos/zm_factory.rb +2 -2
  22. data/lib/rgeo/wkrep/wkb_parser.rb +35 -36
  23. data/lib/rgeo/wkrep/wkt_parser.rb +36 -38
  24. data/test/active_record/common_setup_methods.rb +129 -0
  25. data/test/active_record/readme.txt +10 -0
  26. data/test/active_record/tc_mysqlspatial.rb +22 -71
  27. data/test/active_record/tc_postgis.rb +282 -0
  28. data/test/active_record/tc_spatialite.rb +198 -0
  29. data/test/coord_sys/tc_proj4.rb +12 -5
  30. data/test/projected_geography/tc_geometry_collection.rb +1 -1
  31. data/test/projected_geography/tc_line_string.rb +1 -1
  32. data/test/projected_geography/tc_multi_line_string.rb +1 -1
  33. data/test/projected_geography/tc_multi_point.rb +1 -1
  34. data/test/projected_geography/tc_multi_polygon.rb +2 -2
  35. data/test/projected_geography/tc_point.rb +4 -4
  36. data/test/projected_geography/tc_polygon.rb +1 -1
  37. data/test/simple_mercator/tc_geometry_collection.rb +1 -1
  38. data/test/simple_mercator/tc_line_string.rb +1 -1
  39. data/test/simple_mercator/tc_multi_line_string.rb +1 -1
  40. data/test/simple_mercator/tc_multi_point.rb +1 -1
  41. data/test/simple_mercator/tc_multi_polygon.rb +2 -2
  42. data/test/simple_mercator/tc_point.rb +4 -4
  43. data/test/simple_mercator/tc_polygon.rb +1 -1
  44. data/test/simple_mercator/tc_window.rb +1 -1
  45. data/test/spherical_geography/tc_geometry_collection.rb +1 -1
  46. data/test/spherical_geography/tc_line_string.rb +1 -1
  47. data/test/spherical_geography/tc_multi_line_string.rb +1 -1
  48. data/test/spherical_geography/tc_multi_point.rb +1 -1
  49. data/test/spherical_geography/tc_multi_polygon.rb +2 -2
  50. data/test/spherical_geography/tc_point.rb +4 -4
  51. data/test/spherical_geography/tc_polygon.rb +1 -1
  52. data/test/tc_oneoff.rb +3 -3
  53. data/test/wkrep/tc_wkb_parser.rb +14 -14
  54. data/test/wkrep/tc_wkt_parser.rb +37 -45
  55. metadata +10 -3
@@ -0,0 +1,488 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # SpatiaLite adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'rgeo/active_record/common'
38
+ require 'active_record/connection_adapters/sqlite3_adapter'
39
+
40
+
41
+ module ActiveRecord
42
+
43
+ class Base
44
+
45
+
46
+ # Create a spatialite connection adapter.
47
+
48
+
49
+ def self.spatialite_connection(config_)
50
+ unless 'spatialite' == config_[:adapter]
51
+ raise ::ArgumentError, 'adapter name should be "spatialite"'
52
+ end
53
+ unless config_[:database]
54
+ raise ::ArgumentError, "No database file specified. Missing argument: database"
55
+ end
56
+
57
+ # Allow database path relative to Rails.root, but only if
58
+ # the database path is not the special path that tells
59
+ # Sqlite to build a database only in memory.
60
+ if defined?(::Rails.root) && ':memory:' != config_[:database]
61
+ config_[:database] = ::File.expand_path(config_[:database], ::Rails.root)
62
+ end
63
+
64
+ unless self.class.const_defined?(:SQLite3)
65
+ require_library_or_gem('sqlite3')
66
+ end
67
+ db_ = ::SQLite3::Database.new(config_[:database], :results_as_hash => true)
68
+ db_.busy_timeout(config_[:timeout]) unless config_[:timeout].nil?
69
+
70
+ # Load SpatiaLite
71
+ path_ = config_[:libspatialite]
72
+ if path_ && (!::File.file?(path_) || !::File.readable?(path_))
73
+ raise "Cannot read libspatialite library at #{path_}"
74
+ end
75
+ unless path_
76
+ prefixes_ = ['/usr/local/spatialite', '/usr/local/libspatialite', '/usr/local', '/opt/local', '/sw/local', '/usr']
77
+ suffixes_ = ['so', 'dylib'].join(',')
78
+ prefixes_.each do |prefix_|
79
+ pa_ = ::Dir.glob("#{prefix_}/lib/libspatialite.{#{suffixes_}}")
80
+ if pa_.size > 0
81
+ path_ = pa_.first
82
+ break
83
+ end
84
+ end
85
+ end
86
+ unless path_
87
+ raise 'Cannot find libspatialite in the usual places. Please provide the path in the "libspatialite" config parameter.'
88
+ end
89
+ db_.enable_load_extension(1)
90
+ db_.load_extension(path_)
91
+
92
+ ConnectionAdapters::SpatiaLiteAdapter.new(db_, logger, config_)
93
+ end
94
+
95
+
96
+ end
97
+
98
+
99
+ module ConnectionAdapters # :nodoc:
100
+
101
+
102
+ class SpatiaLiteAdapter < SQLite3Adapter # :nodoc:
103
+
104
+
105
+ ADAPTER_NAME = 'SpatiaLite'.freeze
106
+
107
+ @@native_database_types = nil
108
+
109
+
110
+ def native_database_types
111
+ unless @@native_database_types
112
+ @@native_database_types = super.dup
113
+ @@native_database_types.merge!(:geometry => {:name => "geometry"}, :point => {:name => "point"}, :line_string => {:name => "linestring"}, :polygon => {:name => "polygon"}, :geometry_collection => {:name => "geometrycollection"}, :multi_point => {:name => "multipoint"}, :multi_line_string => {:name => "multilinestring"}, :multi_polygon => {:name => "multipolygon"})
114
+ end
115
+ @@native_database_types
116
+ end
117
+
118
+
119
+ def adapter_name
120
+ ADAPTER_NAME
121
+ end
122
+
123
+
124
+ def spatialite_version
125
+ @spatialite_version ||= SQLiteAdapter::Version.new(select_value('SELECT spatialite_version()'))
126
+ end
127
+
128
+
129
+ def quote(value_, column_=nil)
130
+ if ::RGeo::Feature::Geometry.check_type(value_)
131
+ "GeomFromWKB(X'#{::RGeo::WKRep::WKBGenerator.new(:hex_format => true).generate(value_)}', #{value_.srid})"
132
+ else
133
+ super
134
+ end
135
+ end
136
+
137
+
138
+ def columns(table_name_, name_=nil) #:nodoc:
139
+ spatial_info_ = spatial_column_info(table_name_)
140
+ table_structure(table_name_).map do |field_|
141
+ col_ = SpatialColumn.new(field_['name'], field_['dflt_value'], field_['type'], field_['notnull'].to_i == 0)
142
+ info_ = spatial_info_[field_['name']]
143
+ if info_
144
+ col_.set_srid(info_[:srid])
145
+ end
146
+ col_
147
+ end
148
+ end
149
+
150
+
151
+ def spatial_indexes(table_name_, name_=nil)
152
+ table_name_ = table_name_.to_s
153
+ names_ = select_values("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'idx_#{quote_string(table_name_)}_%' AND rootpage=0") || []
154
+ names_.map do |name_|
155
+ col_name_ = name_.sub("idx_#{table_name_}_", '')
156
+ ::RGeo::ActiveRecord::Common::IndexDefinition.new(table_name_, name_, false, [col_name_], [], true)
157
+ end
158
+ end
159
+
160
+
161
+ def create_table(table_name_, options_={})
162
+ table_name_ = table_name_.to_s
163
+ table_definition_ = SpatialTableDefinition.new(self)
164
+ table_definition_.primary_key(options_[:primary_key] || ::ActiveRecord::Base.get_primary_key(table_name_.singularize)) unless options_[:id] == false
165
+ yield table_definition_ if block_given?
166
+ if options_[:force] && table_exists?(table_name_)
167
+ drop_table(table_name_, options_)
168
+ end
169
+
170
+ create_sql_ = "CREATE#{' TEMPORARY' if options_[:temporary]} TABLE "
171
+ create_sql_ << "#{quote_table_name(table_name_)} ("
172
+ create_sql_ << table_definition_.to_sql
173
+ create_sql_ << ") #{options_[:options]}"
174
+ execute create_sql_
175
+
176
+ table_definition_.spatial_columns.each do |col_|
177
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_)}', '#{quote_string(col_.name)}', #{col_.srid}, '#{quote_string(col_.type.to_s.gsub('_','').upcase)}', 'XY', #{col_.null ? 0 : 1})")
178
+ end
179
+ end
180
+
181
+
182
+ def drop_table(table_name_, options_={})
183
+ execute("DELETE from geometry_columns where f_table_name='#{quote_string(table_name_.to_s)}'")
184
+ super
185
+ end
186
+
187
+
188
+ def add_column(table_name_, column_name_, type_, options_={})
189
+ if ::RGeo::ActiveRecord::GEOMETRY_TYPES.include?(type_.to_sym)
190
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_.to_s)}', '#{quote_string(column_name_.to_s)}', #{options_[:srid].to_i}, '#{quote_string(type_.to_s)}', 'XY', #{options_[:null] == false ? 0 : 1})")
191
+ else
192
+ super
193
+ end
194
+ end
195
+
196
+
197
+ def add_index(table_name_, column_name_, options_={})
198
+ if options_[:spatial]
199
+ column_name_ = column_name_.first if column_name_.kind_of?(::Array) && column_name_.size == 1
200
+ table_name_ = table_name_.to_s
201
+ column_name_ = column_name_.to_s
202
+ spatial_info_ = spatial_column_info(table_name_)
203
+ unless spatial_info_[column_name_]
204
+ raise ::ArgumentError, "Can't create spatial index because column '#{column_name_}' in table '#{table_name_}' is not a geometry column"
205
+ end
206
+ result_ = select_value("SELECT CreateSpatialIndex('#{quote_string(table_name_)}', '#{quote_string(column_name_)}')").to_i
207
+ if result_ == 0
208
+ raise ::ArgumentError, "Spatial index already exists on table '#{table_name_}', column '#{column_name_}'"
209
+ end
210
+ result_
211
+ else
212
+ super
213
+ end
214
+ end
215
+
216
+
217
+ def remove_index(table_name_, options_={})
218
+ if options_[:spatial]
219
+ column_ = options_[:column]
220
+ unless column_
221
+ raise ::ArgumentError, "You need to specify a column to remove a spatial index."
222
+ end
223
+ table_name_ = table_name_.to_s
224
+ column_ = column_.to_s
225
+ spatial_info_ = spatial_column_info(table_name_)
226
+ unless spatial_info_[column_]
227
+ raise ::ArgumentError, "Can't remove spatial index because column '#{column_name_}' in table '#{table_name_}' is not a geometry column"
228
+ end
229
+ index_name_ = "idx_#{table_name_}_#{column_}"
230
+ has_index_ = select_value("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='#{quote_string(index_name_)}'").to_i > 0
231
+ unless has_index_
232
+ raise ::ArgumentError, "Spatial index not present on table '#{table_name_}', column '#{column_name_}'"
233
+ end
234
+ execute("SELECT DisableSpatialIndex('#{quote_string(table_name_)}', '#{quote_string(column_)}')")
235
+ execute("DROP TABLE #{quote_table_name(index_name_)}")
236
+ else
237
+ super
238
+ end
239
+ end
240
+
241
+
242
+ def spatial_column_info(table_name_)
243
+ info_ = execute("SELECT * FROM geometry_columns WHERE f_table_name='#{quote_string(table_name_.to_s)}'")
244
+ result_ = {}
245
+ info_.each do |row_|
246
+ result_[row_['f_geometry_column']] = {
247
+ :name => row_['f_geometry_column'],
248
+ :type => row_['type'],
249
+ :dimension => row_['coord_dimension'],
250
+ :srid => row_['srid'],
251
+ :has_index => row_['spatial_index_enabled'],
252
+ }
253
+ end
254
+ result_
255
+ end
256
+
257
+
258
+ class SpatialTableDefinition < ConnectionAdapters::TableDefinition # :nodoc:
259
+
260
+ attr_reader :spatial_columns
261
+
262
+ def initialize(base_)
263
+ super
264
+ end
265
+
266
+ def column(name_, type_, options_={})
267
+ super
268
+ col_ = self[name_]
269
+ if ::RGeo::ActiveRecord::GEOMETRY_TYPES.include?(col_.type.to_sym)
270
+ col_.extend(GeometricColumnDefinitionMethods) unless col_.respond_to?(:srid)
271
+ col_.set_srid(options_[:srid].to_i)
272
+ end
273
+ self
274
+ end
275
+
276
+ def to_sql
277
+ @columns.find_all{ |c_| !c_.respond_to?(:srid) }.map{ |c_| c_.to_sql } * ', '
278
+ end
279
+
280
+ def spatial_columns
281
+ @columns.find_all{ |c_| c_.respond_to?(:srid) }
282
+ end
283
+
284
+ end
285
+
286
+
287
+ module GeometricColumnDefinitionMethods # :nodoc:
288
+
289
+ def srid
290
+ defined?(@srid) ? @srid : 4326
291
+ end
292
+
293
+ def set_srid(value_)
294
+ @srid = value_
295
+ end
296
+
297
+ end
298
+
299
+
300
+ class SpatialColumn < ConnectionAdapters::SQLiteColumn # :nodoc:
301
+
302
+
303
+ def initialize(name_, default_, sql_type_=nil, null_=true)
304
+ super(name_, default_, sql_type_, null_)
305
+ @geometric_type = ::RGeo::ActiveRecord::Common.geometric_type_from_name(sql_type_)
306
+ @ar_class = ::ActiveRecord::Base
307
+ @srid = 0
308
+ end
309
+
310
+
311
+ def set_ar_class(val_)
312
+ @ar_class = val_
313
+ end
314
+
315
+ def set_srid(val_)
316
+ @srid = val_
317
+ end
318
+
319
+
320
+ attr_reader :srid
321
+ attr_reader :geometric_type
322
+
323
+
324
+ def spatial?
325
+ type == :geometry
326
+ end
327
+
328
+
329
+ def klass
330
+ type == :geometry ? ::RGeo::Feature::Geometry : super
331
+ end
332
+
333
+
334
+ def type_cast(value_)
335
+ type == :geometry ? SpatialColumn.string_to_geometry(value_, @ar_class, @srid) : super
336
+ end
337
+
338
+
339
+ def type_cast_code(var_name_)
340
+ type == :geometry ? "::ActiveRecord::ConnectionAdapters::SpatiaLiteAdapter::SpatialColumn.string_to_geometry(#{var_name_}, self.class, #{@srid})" : super
341
+ end
342
+
343
+
344
+ private
345
+
346
+
347
+ def simplified_type(sql_type_)
348
+ sql_type_ =~ /geometry|point|linestring|polygon/i ? :geometry : super
349
+ end
350
+
351
+
352
+ def self.string_to_geometry(str_, ar_class_, column_srid_)
353
+ case str_
354
+ when ::RGeo::Feature::Geometry
355
+ str_
356
+ when ::String
357
+ if str_.length == 0
358
+ nil
359
+ else
360
+ factory_generator_ = ar_class_.rgeo_factory_generator
361
+ if str_[0,1] == "\x00"
362
+ NativeFormatParser.new(factory_generator_).parse(str_) rescue nil
363
+ else
364
+ ::RGeo::WKRep::WKTParser.new(factory_generator_.call(:srid => column_srid_), :support_ewkt => true).parse(str_)
365
+ end
366
+ end
367
+ else
368
+ nil
369
+ end
370
+ end
371
+
372
+
373
+ end
374
+
375
+
376
+ class NativeFormatParser # :nodoc:
377
+
378
+
379
+ def initialize(factory_generator_)
380
+ @factory_generator = factory_generator_
381
+ end
382
+
383
+
384
+ def parse(data_)
385
+ @little_endian = data_[1,1] == "\x01"
386
+ srid_ = data_[2,4].unpack(@little_endian ? 'V' : 'N').first
387
+ @cur_factory = @factory_generator.call(:srid => srid_)
388
+ begin
389
+ _start_scanner(data_)
390
+ obj_ = _parse_object(false)
391
+ _get_byte(0xfe)
392
+ ensure
393
+ _clean_scanner
394
+ end
395
+ obj_
396
+ end
397
+
398
+
399
+ def _parse_object(contained_)
400
+ _get_byte(contained_ ? 0x69 : 0x7c)
401
+ type_code_ = _get_integer
402
+ case type_code_
403
+ when 1
404
+ coords_ = _get_doubles(2)
405
+ @cur_factory.point(*coords_)
406
+ when 2
407
+ _parse_line_string
408
+ when 3
409
+ interior_rings_ = (1.._get_integer).map{ _parse_line_string }
410
+ exterior_ring_ = interior_rings_.shift || @cur_factory.linear_ring([])
411
+ @cur_factory.polygon(exterior_ring_, interior_rings_)
412
+ when 4
413
+ @cur_factory.multi_point((1.._get_integer).map{ _parse_object(1) })
414
+ when 5
415
+ @cur_factory.multi_line_string((1.._get_integer).map{ _parse_object(2) })
416
+ when 6
417
+ @cur_factory.multi_polygon((1.._get_integer).map{ _parse_object(3) })
418
+ when 7
419
+ @cur_factory.collection((1.._get_integer).map{ _parse_object(true) })
420
+ else
421
+ raise ::RGeo::Error::ParseError, "Unknown type value: #{type_code_}."
422
+ end
423
+ end
424
+
425
+
426
+ def _parse_line_string
427
+ count_ = _get_integer
428
+ coords_ = _get_doubles(2 * count_)
429
+ @cur_factory.line_string((0...count_).map{ |i_| @cur_factory.point(*coords_[2*i_,2]) })
430
+ end
431
+
432
+
433
+ def _start_scanner(data_)
434
+ @_data = data_
435
+ @_len = data_.length
436
+ @_pos = 38
437
+ end
438
+
439
+
440
+ def _clean_scanner
441
+ @_data = nil
442
+ end
443
+
444
+
445
+ def _get_byte(expect_=nil)
446
+ if @_pos + 1 > @_len
447
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill 1 byte"
448
+ end
449
+ str_ = @_data[@_pos, 1]
450
+ @_pos += 1
451
+ val_ = str_.unpack("C").first
452
+ if expect_ && expect_ != val_
453
+ raise ::RGeo::Error::ParseError, "Expected byte 0x#{expect_.to_s(16)} but got 0x#{val_.to_s(16)}"
454
+ end
455
+ val_
456
+ end
457
+
458
+
459
+ def _get_integer
460
+ if @_pos + 4 > @_len
461
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill 1 integer"
462
+ end
463
+ str_ = @_data[@_pos, 4]
464
+ @_pos += 4
465
+ str_.unpack("#{@little_endian ? 'V' : 'N'}").first
466
+ end
467
+
468
+
469
+ def _get_doubles(count_)
470
+ len_ = 8 * count_
471
+ if @_pos + len_ > @_len
472
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill #{count_} doubles"
473
+ end
474
+ str_ = @_data[@_pos, len_]
475
+ @_pos += len_
476
+ str_.unpack("#{@little_endian ? 'E' : 'G'}*")
477
+ end
478
+
479
+
480
+ end
481
+
482
+
483
+ end
484
+
485
+ end
486
+
487
+
488
+ end