rgeo 0.1.20 → 0.1.21

Sign up to get free protection for your applications and to get access to all the features.
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