activerecord-spatialite-adapter 0.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.
@@ -0,0 +1,8 @@
1
+ === 0.2.0 / 2010-12-07
2
+
3
+ * Initial public alpha release. Spun activerecord-spatialite-adapter off from the core rgeo gem.
4
+ * You can now set the factory for a specific column by name.
5
+ * Dropping a table that had spatial indexes tied to it didn't remove the index tables. Fixed.
6
+ * You can now remove spatial indexes by name.
7
+
8
+ For earlier history, see the History file for the rgeo gem.
@@ -0,0 +1,155 @@
1
+ == SpatiaLite \ActiveRecord Adapter
2
+
3
+ The SpatiaLite \ActiveRecord Adapter is an \ActiveRecord connection
4
+ adapter based on the standard sqlite3 adapter. It extends the standard
5
+ adapter to provide support for spatial extensions using SpatiaLite, using
6
+ the {RGeo}[http://github.com/dazuma/rgeo] library to represent spatial
7
+ data in Ruby. Like the standard sqlite3 adapter, this adapter requires
8
+ the sqlite3-ruby gem.
9
+
10
+ === Usage
11
+
12
+ To use this adapter, add this gem, "activerecord-spatialite-adapter",
13
+ to your Gemfile, and then request the adapter name "spatialite" in
14
+ your database connection configuration (which, for a Rails application,
15
+ is in the config/database.yml file). The other database connection
16
+ configuration parameters are the same as for the stock sqlite3 adapter,
17
+ with the exception of one additional parameter, <tt>libspatialite</tt>,
18
+ which should be set to the full path to the libspatialite shared library,
19
+ if it is not installed in a standard place (such as /usr/local or
20
+ /opt/local).
21
+
22
+ First, this adapter extends the migration syntax to support creating
23
+ spatial columns and indexes. To create a spatial column, use the
24
+ <tt>:geometry</tt> type, or any of the OGC spatial types such as
25
+ <tt>:point</tt> or <tt>:line_string</tt>. To create a spatial index, set
26
+ the <tt>:spatial</tt> option to true.
27
+
28
+ Examples:
29
+
30
+ create_table :spatial_table do |t|
31
+ t.column :latlon, :point # or t.point :latlon
32
+ t.line_string :path
33
+ t.geometry :shape
34
+ end
35
+ change_table :spatial_table do |t|
36
+ t.index :latlon, :spatial => true
37
+ end
38
+
39
+ When this adapter is in use, spatial attributes in your \ActiveRecord
40
+ objects will have RGeo geometry values. You can set spatial attributes
41
+ either to RGeo geometry objects, or to strings in WKT (well-known text)
42
+ format, which the adapter will automatically convert to geometry objects.
43
+
44
+ To specify the RGeo geometry factory, you can either set an explicit
45
+ factory for a column, or provide a factory generator that will yield the
46
+ appropriate factory for the table's spatial columns based on the value.
47
+ For the former, call the set_rgeo_factory_for_column class method on your
48
+ \ActiveRecord class. For the latter, set the rgeo_factory_generator class
49
+ attribute. This generator should understand at least the <tt>:srid</tt>
50
+ option, which will be provided based on the column's specified SRID. Note
51
+ that the spatialite adapter does not currently support Z or M coordinates,
52
+ as it's unclear to me whether SpatiaLite itself supports them. The
53
+ set_rgeo_factory_for_column and rgeo_factory_generator methods are
54
+ actually implemented and documented in the "rgeo-activerecord" gem.
55
+
56
+ Examples, given the spatial table defined above:
57
+
58
+ class SpatialTable < ActiveRecord::Base
59
+
60
+ # By default, use the GEOS implementation for spatial columns.
61
+ self.rgeo_factory_generator = RGeo::Geos.method(:factory)
62
+
63
+ # But use a geographic implementation for the :latlon column.
64
+ set_rgeo_factory_for_column(:latlon, RGeo::Geographic.spherical_factory)
65
+
66
+ end
67
+
68
+ Now you can interact with the data using the RGeo types:
69
+
70
+ rec = SpatialTable.new
71
+ rec.latlon = 'POINT(-122 47)' # You can set by feature object or WKT.
72
+ loc = rec.latlon # Accessing always returns a feature object, in
73
+ # this case, a geographic that understands latitude.
74
+ loc.latitude # => 47
75
+ rec.shape = loc # the factory for the :shape column is GEOS, so the
76
+ # value will be cast from geographic to GEOS.
77
+ RGeo::Geos.is_geos?(rec.shape) # => true
78
+
79
+ === Installation
80
+
81
+ This adapter has the following requirements:
82
+
83
+ * Ruby 1.8.7 or later. Ruby 1.9.2 or later preferred.
84
+ * SpatiaLite 2.3 or later (2.4 recommended).
85
+ * \ActiveRecord 3.0.3 or later. Earlier versions will not work.
86
+ * rgeo gem 0.2.0 or later.
87
+ * rgeo-activerecord gem 0.2.0 or later.
88
+ * sqlite3 gem 1.3 or later.
89
+
90
+ Install this adapter as a gem:
91
+
92
+ gem install activerecord-spatialite-adapter
93
+
94
+ See the README for the "rgeo" gem, a required dependency, for further
95
+ installation information.
96
+
97
+ === Known bugs and limitations
98
+
99
+ The spatialite adapter works in principle, but there are a few known holes
100
+ in the functionality. Notably, things that require the alter_table
101
+ mechanism may not function properly, because the current sqlite3
102
+ implementation doesn't properly preserve triggers. This means, among
103
+ other things, removing columns in tables with spatial information can
104
+ cause the remaining spatial columns to fail. However, most simple things
105
+ work, including creating tables with geometric columns, adding geometric
106
+ columns to existing tables, and creating and removing spatial R*tree
107
+ indexes. Note that this adapter is not yet well tested.
108
+
109
+ === Development and support
110
+
111
+ Documentation is available at http://virtuoso.rubyforge.org/activerecord-spatialite-adapter/README_rdoc.html
112
+
113
+ Source code is hosted on Github at http://github.com/dazuma/activerecord-spatialite-adapter
114
+
115
+ Contributions are welcome. Fork the project on Github.
116
+
117
+ Report bugs on Github issues at http://github.org/dazuma/activerecord-spatialite-adapter/issues
118
+
119
+ Contact the author at dazuma at gmail dot com.
120
+
121
+ === Acknowledgments
122
+
123
+ RGeo is written by Daniel Azuma (http://www.daniel-azuma.com).
124
+
125
+ Development of RGeo is sponsored by GeoPage, Inc. (http://www.geopage.com).
126
+
127
+ === License
128
+
129
+ Copyright 2010 Daniel Azuma
130
+
131
+ All rights reserved.
132
+
133
+ Redistribution and use in source and binary forms, with or without
134
+ modification, are permitted provided that the following conditions are met:
135
+
136
+ * Redistributions of source code must retain the above copyright notice,
137
+ this list of conditions and the following disclaimer.
138
+ * Redistributions in binary form must reproduce the above copyright notice,
139
+ this list of conditions and the following disclaimer in the documentation
140
+ and/or other materials provided with the distribution.
141
+ * Neither the name of the copyright holder, nor the names of any other
142
+ contributors to this software, may be used to endorse or promote products
143
+ derived from this software without specific prior written permission.
144
+
145
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
146
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
147
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
148
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
149
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
150
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
151
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
152
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
153
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
154
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
155
+ POSSIBILITY OF SUCH DAMAGE.
data/Version ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,518 @@
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'
38
+ require 'active_record/connection_adapters/sqlite3_adapter'
39
+
40
+
41
+ # :stopdoc:
42
+
43
+ module Arel
44
+ module Visitors
45
+ VISITORS['spatialite'] = ::Arel::Visitors::SQLite
46
+ end
47
+ end
48
+
49
+ # :startdoc:
50
+
51
+
52
+ # The activerecord-spatialite-adapter gem installs the *spatialite*
53
+ # connection adapter into ActiveRecord.
54
+
55
+ module ActiveRecord
56
+
57
+
58
+ # ActiveRecord looks for the spatialite_connection factory method in
59
+ # this class.
60
+
61
+ class Base
62
+
63
+
64
+ # Create a spatialite connection adapter.
65
+
66
+ def self.spatialite_connection(config_)
67
+ unless 'spatialite' == config_[:adapter]
68
+ raise ::ArgumentError, 'adapter name should be "spatialite"'
69
+ end
70
+ unless config_[:database]
71
+ raise ::ArgumentError, "No database file specified. Missing argument: database"
72
+ end
73
+
74
+ # Allow database path relative to Rails.root, but only if
75
+ # the database path is not the special path that tells
76
+ # Sqlite to build a database only in memory.
77
+ if defined?(::Rails.root) && ':memory:' != config_[:database]
78
+ config_[:database] = ::File.expand_path(config_[:database], ::Rails.root)
79
+ end
80
+
81
+ unless self.class.const_defined?(:SQLite3)
82
+ require_library_or_gem('sqlite3')
83
+ end
84
+ db_ = ::SQLite3::Database.new(config_[:database], :results_as_hash => true)
85
+ db_.busy_timeout(config_[:timeout]) unless config_[:timeout].nil?
86
+
87
+ # Load SpatiaLite
88
+ path_ = config_[:libspatialite]
89
+ if path_ && (!::File.file?(path_) || !::File.readable?(path_))
90
+ raise "Cannot read libspatialite library at #{path_}"
91
+ end
92
+ unless path_
93
+ prefixes_ = ['/usr/local/spatialite', '/usr/local/libspatialite', '/usr/local', '/opt/local', '/sw/local', '/usr']
94
+ suffixes_ = ['so', 'dylib'].join(',')
95
+ prefixes_.each do |prefix_|
96
+ pa_ = ::Dir.glob("#{prefix_}/lib/libspatialite.{#{suffixes_}}")
97
+ if pa_.size > 0
98
+ path_ = pa_.first
99
+ break
100
+ end
101
+ end
102
+ end
103
+ unless path_
104
+ raise 'Cannot find libspatialite in the usual places. Please provide the path in the "libspatialite" config parameter.'
105
+ end
106
+ db_.enable_load_extension(1)
107
+ db_.load_extension(path_)
108
+
109
+ ConnectionAdapters::SpatiaLiteAdapter.new(db_, logger, config_)
110
+ end
111
+
112
+
113
+ end
114
+
115
+
116
+ module ConnectionAdapters # :nodoc:
117
+
118
+
119
+ class SpatiaLiteAdapter < SQLite3Adapter # :nodoc:
120
+
121
+
122
+ ADAPTER_NAME = 'SpatiaLite'.freeze
123
+
124
+ @@native_database_types = nil
125
+
126
+
127
+ def native_database_types
128
+ unless @@native_database_types
129
+ @@native_database_types = super.dup
130
+ @@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"})
131
+ end
132
+ @@native_database_types
133
+ end
134
+
135
+
136
+ def adapter_name
137
+ ADAPTER_NAME
138
+ end
139
+
140
+
141
+ def spatialite_version
142
+ @spatialite_version ||= SQLiteAdapter::Version.new(select_value('SELECT spatialite_version()'))
143
+ end
144
+
145
+
146
+ def quote(value_, column_=nil)
147
+ if ::RGeo::Feature::Geometry.check_type(value_)
148
+ "GeomFromWKB(X'#{::RGeo::WKRep::WKBGenerator.new(:hex_format => true).generate(value_)}', #{value_.srid})"
149
+ else
150
+ super
151
+ end
152
+ end
153
+
154
+
155
+ def columns(table_name_, name_=nil) #:nodoc:
156
+ spatial_info_ = spatial_column_info(table_name_)
157
+ table_structure(table_name_).map do |field_|
158
+ col_ = SpatialColumn.new(field_['name'], field_['dflt_value'], field_['type'], field_['notnull'].to_i == 0)
159
+ info_ = spatial_info_[field_['name']]
160
+ if info_
161
+ col_.set_srid(info_[:srid])
162
+ end
163
+ col_
164
+ end
165
+ end
166
+
167
+
168
+ def spatial_indexes(table_name_, name_=nil)
169
+ table_name_ = table_name_.to_s
170
+ names_ = select_values("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'idx_#{quote_string(table_name_)}_%' AND rootpage=0") || []
171
+ names_.map do |name_|
172
+ col_name_ = name_.sub("idx_#{table_name_}_", '')
173
+ ::RGeo::ActiveRecord::SpatialIndexDefinition.new(table_name_, name_, false, [col_name_], [], true)
174
+ end
175
+ end
176
+
177
+
178
+ def create_table(table_name_, options_={})
179
+ table_name_ = table_name_.to_s
180
+ table_definition_ = SpatialTableDefinition.new(self)
181
+ table_definition_.primary_key(options_[:primary_key] || ::ActiveRecord::Base.get_primary_key(table_name_.singularize)) unless options_[:id] == false
182
+ yield table_definition_ if block_given?
183
+ if options_[:force] && table_exists?(table_name_)
184
+ drop_table(table_name_, options_)
185
+ end
186
+
187
+ create_sql_ = "CREATE#{' TEMPORARY' if options_[:temporary]} TABLE "
188
+ create_sql_ << "#{quote_table_name(table_name_)} ("
189
+ create_sql_ << table_definition_.to_sql
190
+ create_sql_ << ") #{options_[:options]}"
191
+ execute create_sql_
192
+
193
+ table_definition_.spatial_columns.each do |col_|
194
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_)}', '#{quote_string(col_.name.to_s)}', #{col_.srid}, '#{quote_string(col_.type.to_s.gsub('_','').upcase)}', 'XY', #{col_.null ? 0 : 1})")
195
+ end
196
+ end
197
+
198
+
199
+ def drop_table(table_name_, options_={})
200
+ spatial_indexes(table_name_).each do |index_|
201
+ remove_index(table_name_, :spatial => true, :column => index_.columns[0])
202
+ end
203
+ execute("DELETE from geometry_columns where f_table_name='#{quote_string(table_name_.to_s)}'")
204
+ super
205
+ end
206
+
207
+
208
+ def add_column(table_name_, column_name_, type_, options_={})
209
+ if ::RGeo::ActiveRecord::GEOMETRY_TYPES.include?(type_.to_sym)
210
+ 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})")
211
+ else
212
+ super
213
+ end
214
+ end
215
+
216
+
217
+ def add_index(table_name_, column_name_, options_={})
218
+ if options_[:spatial]
219
+ column_name_ = column_name_.first if column_name_.kind_of?(::Array) && column_name_.size == 1
220
+ table_name_ = table_name_.to_s
221
+ column_name_ = column_name_.to_s
222
+ spatial_info_ = spatial_column_info(table_name_)
223
+ unless spatial_info_[column_name_]
224
+ raise ::ArgumentError, "Can't create spatial index because column '#{column_name_}' in table '#{table_name_}' is not a geometry column"
225
+ end
226
+ result_ = select_value("SELECT CreateSpatialIndex('#{quote_string(table_name_)}', '#{quote_string(column_name_)}')").to_i
227
+ if result_ == 0
228
+ raise ::ArgumentError, "Spatial index already exists on table '#{table_name_}', column '#{column_name_}'"
229
+ end
230
+ result_
231
+ else
232
+ super
233
+ end
234
+ end
235
+
236
+
237
+ def remove_index(table_name_, options_={})
238
+ if options_[:spatial]
239
+ table_name_ = table_name_.to_s
240
+ column_ = options_[:column]
241
+ if column_
242
+ column_ = column_[0] if column_.kind_of?(::Array)
243
+ column_ = column_.to_s
244
+ else
245
+ index_name_ = options_[:name]
246
+ unless index_name_
247
+ raise ::ArgumentError, "You need to specify a column or index name to remove a spatial index."
248
+ end
249
+ if index_name_ =~ /^idx_#{table_name_}_(\w+)$/
250
+ column_ = $1
251
+ else
252
+ raise ::ArgumentError, "Unknown spatial index name: #{index_name_.inspect}."
253
+ end
254
+ end
255
+ spatial_info_ = spatial_column_info(table_name_)
256
+ unless spatial_info_[column_]
257
+ raise ::ArgumentError, "Can't remove spatial index because column '#{column_}' in table '#{table_name_}' is not a geometry column"
258
+ end
259
+ index_name_ = "idx_#{table_name_}_#{column_}"
260
+ has_index_ = select_value("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='#{quote_string(index_name_)}'").to_i > 0
261
+ unless has_index_
262
+ raise ::ArgumentError, "Spatial index not present on table '#{table_name_}', column '#{column_}'"
263
+ end
264
+ execute("SELECT DisableSpatialIndex('#{quote_string(table_name_)}', '#{quote_string(column_)}')")
265
+ execute("DROP TABLE #{quote_table_name(index_name_)}")
266
+ else
267
+ super
268
+ end
269
+ end
270
+
271
+
272
+ def spatial_column_info(table_name_)
273
+ info_ = execute("SELECT * FROM geometry_columns WHERE f_table_name='#{quote_string(table_name_.to_s)}'")
274
+ result_ = {}
275
+ info_.each do |row_|
276
+ result_[row_['f_geometry_column']] = {
277
+ :name => row_['f_geometry_column'],
278
+ :type => row_['type'],
279
+ :dimension => row_['coord_dimension'],
280
+ :srid => row_['srid'],
281
+ :has_index => row_['spatial_index_enabled'],
282
+ }
283
+ end
284
+ result_
285
+ end
286
+
287
+
288
+ class SpatialTableDefinition < ConnectionAdapters::TableDefinition # :nodoc:
289
+
290
+ attr_reader :spatial_columns
291
+
292
+ def initialize(base_)
293
+ super
294
+ end
295
+
296
+ def column(name_, type_, options_={})
297
+ super
298
+ col_ = self[name_]
299
+ if ::RGeo::ActiveRecord::GEOMETRY_TYPES.include?(col_.type.to_sym)
300
+ col_.extend(GeometricColumnDefinitionMethods) unless col_.respond_to?(:srid)
301
+ col_.set_srid(options_[:srid].to_i)
302
+ end
303
+ self
304
+ end
305
+
306
+ def to_sql
307
+ @columns.find_all{ |c_| !c_.respond_to?(:srid) }.map{ |c_| c_.to_sql } * ', '
308
+ end
309
+
310
+ def spatial_columns
311
+ @columns.find_all{ |c_| c_.respond_to?(:srid) }
312
+ end
313
+
314
+ end
315
+
316
+
317
+ module GeometricColumnDefinitionMethods # :nodoc:
318
+
319
+ def srid
320
+ defined?(@srid) ? @srid : 4326
321
+ end
322
+
323
+ def set_srid(value_)
324
+ @srid = value_
325
+ end
326
+
327
+ end
328
+
329
+
330
+ class SpatialColumn < ConnectionAdapters::SQLiteColumn # :nodoc:
331
+
332
+
333
+ def initialize(name_, default_, sql_type_=nil, null_=true)
334
+ super(name_, default_, sql_type_, null_)
335
+ @geometric_type = ::RGeo::ActiveRecord.geometric_type_from_name(sql_type_)
336
+ @ar_class = ::ActiveRecord::Base
337
+ @srid = 0
338
+ end
339
+
340
+
341
+ def set_ar_class(val_)
342
+ @ar_class = val_
343
+ end
344
+
345
+ def set_srid(val_)
346
+ @srid = val_
347
+ end
348
+
349
+
350
+ attr_reader :srid
351
+ attr_reader :geometric_type
352
+
353
+
354
+ def spatial?
355
+ type == :geometry
356
+ end
357
+
358
+
359
+ def klass
360
+ type == :geometry ? ::RGeo::Feature::Geometry : super
361
+ end
362
+
363
+
364
+ def type_cast(value_)
365
+ type == :geometry ? SpatialColumn.convert_to_geometry(value_, @ar_class, name, @srid) : super
366
+ end
367
+
368
+
369
+ def type_cast_code(var_name_)
370
+ type == :geometry ? "::ActiveRecord::ConnectionAdapters::SpatiaLiteAdapter::SpatialColumn.convert_to_geometry(#{var_name_}, self.class, #{name.inspect}, #{@srid})" : super
371
+ end
372
+
373
+
374
+ private
375
+
376
+
377
+ def simplified_type(sql_type_)
378
+ sql_type_ =~ /geometry|point|linestring|polygon/i ? :geometry : super
379
+ end
380
+
381
+
382
+ def self.convert_to_geometry(input_, ar_class_, column_name_, column_srid_)
383
+ case input_
384
+ when ::RGeo::Feature::Geometry
385
+ factory_ = ar_class_.rgeo_factory_for_column(column_name_, :srid => column_srid_)
386
+ ::RGeo::Feature.cast(input_, factory_)
387
+ when ::String
388
+ if input_.length == 0
389
+ nil
390
+ else
391
+ factory_ = ar_class_.rgeo_factory_for_column(column_name_, :srid => column_srid_)
392
+ if input_[0,1] == "\x00"
393
+ NativeFormatParser.new(factory_).parse(input_) rescue nil
394
+ else
395
+ ::RGeo::WKRep::WKTParser.new(factory_, :support_ewkt => true).parse(input_)
396
+ end
397
+ end
398
+ else
399
+ nil
400
+ end
401
+ end
402
+
403
+
404
+ end
405
+
406
+
407
+ class NativeFormatParser # :nodoc:
408
+
409
+
410
+ def initialize(factory_)
411
+ @factory = factory_
412
+ end
413
+
414
+
415
+ def parse(data_)
416
+ @little_endian = data_[1,1] == "\x01"
417
+ srid_ = data_[2,4].unpack(@little_endian ? 'V' : 'N').first
418
+ begin
419
+ _start_scanner(data_)
420
+ obj_ = _parse_object(false)
421
+ _get_byte(0xfe)
422
+ ensure
423
+ _clean_scanner
424
+ end
425
+ obj_
426
+ end
427
+
428
+
429
+ def _parse_object(contained_)
430
+ _get_byte(contained_ ? 0x69 : 0x7c)
431
+ type_code_ = _get_integer
432
+ case type_code_
433
+ when 1
434
+ coords_ = _get_doubles(2)
435
+ @factory.point(*coords_)
436
+ when 2
437
+ _parse_line_string
438
+ when 3
439
+ interior_rings_ = (1.._get_integer).map{ _parse_line_string }
440
+ exterior_ring_ = interior_rings_.shift || @factory.linear_ring([])
441
+ @factory.polygon(exterior_ring_, interior_rings_)
442
+ when 4
443
+ @factory.multi_point((1.._get_integer).map{ _parse_object(1) })
444
+ when 5
445
+ @factory.multi_line_string((1.._get_integer).map{ _parse_object(2) })
446
+ when 6
447
+ @factory.multi_polygon((1.._get_integer).map{ _parse_object(3) })
448
+ when 7
449
+ @factory.collection((1.._get_integer).map{ _parse_object(true) })
450
+ else
451
+ raise ::RGeo::Error::ParseError, "Unknown type value: #{type_code_}."
452
+ end
453
+ end
454
+
455
+
456
+ def _parse_line_string
457
+ count_ = _get_integer
458
+ coords_ = _get_doubles(2 * count_)
459
+ @factory.line_string((0...count_).map{ |i_| @factory.point(*coords_[2*i_,2]) })
460
+ end
461
+
462
+
463
+ def _start_scanner(data_)
464
+ @_data = data_
465
+ @_len = data_.length
466
+ @_pos = 38
467
+ end
468
+
469
+
470
+ def _clean_scanner
471
+ @_data = nil
472
+ end
473
+
474
+
475
+ def _get_byte(expect_=nil)
476
+ if @_pos + 1 > @_len
477
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill 1 byte"
478
+ end
479
+ str_ = @_data[@_pos, 1]
480
+ @_pos += 1
481
+ val_ = str_.unpack("C").first
482
+ if expect_ && expect_ != val_
483
+ raise ::RGeo::Error::ParseError, "Expected byte 0x#{expect_.to_s(16)} but got 0x#{val_.to_s(16)}"
484
+ end
485
+ val_
486
+ end
487
+
488
+
489
+ def _get_integer
490
+ if @_pos + 4 > @_len
491
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill 1 integer"
492
+ end
493
+ str_ = @_data[@_pos, 4]
494
+ @_pos += 4
495
+ str_.unpack("#{@little_endian ? 'V' : 'N'}").first
496
+ end
497
+
498
+
499
+ def _get_doubles(count_)
500
+ len_ = 8 * count_
501
+ if @_pos + len_ > @_len
502
+ raise ::RGeo::Error::ParseError, "Not enough bytes left to fulfill #{count_} doubles"
503
+ end
504
+ str_ = @_data[@_pos, len_]
505
+ @_pos += len_
506
+ str_.unpack("#{@little_endian ? 'E' : 'G'}*")
507
+ end
508
+
509
+
510
+ end
511
+
512
+
513
+ end
514
+
515
+ end
516
+
517
+
518
+ end
@@ -0,0 +1,216 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Tests for the MysqlSpatial ActiveRecord adapter
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
+ require 'test/unit'
37
+ require 'rgeo/active_record/adapter_test_helper'
38
+
39
+
40
+ module RGeo
41
+ module ActiveRecord # :nodoc:
42
+ module SpatiaLiteAdapter # :nodoc:
43
+ module Tests # :nodoc:
44
+
45
+ class TestBasic < ::Test::Unit::TestCase # :nodoc:
46
+
47
+
48
+ DATABASE_CONFIG_PATH = ::File.dirname(__FILE__)+'/database.yml'
49
+
50
+ def self.before_open_database(params_)
51
+ database_ = params_[:config][:database]
52
+ dir_ = ::File.dirname(database_)
53
+ ::FileUtils.mkdir_p(dir_) unless dir_ == '.'
54
+ ::FileUtils.rm_f(database_)
55
+ end
56
+
57
+ def self.initialize_database(params_)
58
+ params_[:connection].execute('SELECT InitSpatialMetaData()')
59
+ end
60
+
61
+ include AdapterTestHelper
62
+
63
+
64
+ define_test_methods do
65
+
66
+
67
+ def populate_ar_class(content_)
68
+ klass_ = create_ar_class
69
+ case content_
70
+ when :latlon_point
71
+ klass_.connection.create_table(:spatial_test) do |t_|
72
+ t_.column 'latlon', :point, :srid => 4326
73
+ end
74
+ end
75
+ klass_
76
+ end
77
+
78
+
79
+ def test_meta_data_present
80
+ result_ = DEFAULT_AR_CLASS.connection.select_value("SELECT COUNT(*) FROM spatial_ref_sys").to_i
81
+ assert_not_equal(0, result_)
82
+ end
83
+
84
+
85
+ def test_create_simple_geometry
86
+ klass_ = create_ar_class
87
+ klass_.connection.create_table(:spatial_test) do |t_|
88
+ t_.column 'latlon', :geometry
89
+ end
90
+ assert_equal(1, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
91
+ assert_equal(::RGeo::Feature::Geometry, klass_.columns.last.geometric_type)
92
+ assert(klass_.cached_attributes.include?('latlon'))
93
+ klass_.connection.drop_table(:spatial_test)
94
+ assert_equal(0, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
95
+ end
96
+
97
+
98
+ def test_create_point_geometry
99
+ klass_ = create_ar_class
100
+ klass_.connection.create_table(:spatial_test) do |t_|
101
+ t_.column 'latlon', :point
102
+ end
103
+ assert_equal(::RGeo::Feature::Point, klass_.columns.last.geometric_type)
104
+ assert(klass_.cached_attributes.include?('latlon'))
105
+ end
106
+
107
+
108
+ def test_create_geometry_with_index
109
+ klass_ = create_ar_class
110
+ klass_.connection.create_table(:spatial_test) do |t_|
111
+ t_.column 'latlon', :geometry
112
+ end
113
+ klass_.connection.change_table(:spatial_test) do |t_|
114
+ t_.index([:latlon], :spatial => true)
115
+ end
116
+ assert(klass_.connection.spatial_indexes(:spatial_test).last.spatial)
117
+ assert_equal(1, klass_.connection.select_value("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='idx_spatial_test_latlon'").to_i)
118
+ klass_.connection.drop_table(:spatial_test)
119
+ assert_equal(0, klass_.connection.select_value("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='idx_spatial_test_latlon'").to_i)
120
+ end
121
+
122
+
123
+ def test_set_and_get_point
124
+ klass_ = populate_ar_class(:latlon_point)
125
+ obj_ = klass_.new
126
+ assert_nil(obj_.latlon)
127
+ obj_.latlon = @factory.point(1, 2)
128
+ assert_equal(@factory.point(1, 2), obj_.latlon)
129
+ assert_equal(4326, obj_.latlon.srid)
130
+ end
131
+
132
+
133
+ def test_set_and_get_point_from_wkt
134
+ klass_ = populate_ar_class(:latlon_point)
135
+ obj_ = klass_.new
136
+ assert_nil(obj_.latlon)
137
+ obj_.latlon = 'POINT(1 2)'
138
+ assert_equal(@factory.point(1, 2), obj_.latlon)
139
+ assert_equal(4326, obj_.latlon.srid)
140
+ end
141
+
142
+
143
+ def test_save_and_load_point
144
+ klass_ = populate_ar_class(:latlon_point)
145
+ obj_ = klass_.new
146
+ obj_.latlon = @factory.point(1, 2)
147
+ obj_.save!
148
+ id_ = obj_.id
149
+ obj2_ = klass_.find(id_)
150
+ assert_equal(@factory.point(1, 2), obj2_.latlon)
151
+ assert_equal(4326, obj2_.latlon.srid)
152
+ end
153
+
154
+
155
+ def test_save_and_load_point_from_wkt
156
+ klass_ = populate_ar_class(:latlon_point)
157
+ obj_ = klass_.new
158
+ obj_.latlon = 'POINT(1 2)'
159
+ obj_.save!
160
+ id_ = obj_.id
161
+ obj2_ = klass_.find(id_)
162
+ assert_equal(@factory.point(1, 2), obj2_.latlon)
163
+ assert_equal(4326, obj2_.latlon.srid)
164
+ end
165
+
166
+
167
+ def test_add_column
168
+ klass_ = create_ar_class
169
+ klass_.connection.create_table(:spatial_test) do |t_|
170
+ t_.column('latlon', :geometry)
171
+ end
172
+ klass_.connection.change_table(:spatial_test) do |t_|
173
+ t_.column('geom2', :point, :srid => 4326)
174
+ t_.column('name', :string)
175
+ end
176
+ assert_equal(2, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
177
+ cols_ = klass_.columns
178
+ assert_equal(::RGeo::Feature::Geometry, cols_[-3].geometric_type)
179
+ assert_equal(-1, cols_[-3].srid)
180
+ assert_equal(::RGeo::Feature::Point, cols_[-2].geometric_type)
181
+ assert_equal(4326, cols_[-2].srid)
182
+ assert_nil(cols_[-1].geometric_type)
183
+ end
184
+
185
+
186
+ def test_readme_example
187
+ klass_ = create_ar_class
188
+ klass_.connection.create_table(:spatial_test) do |t_|
189
+ t_.column(:latlon, :point)
190
+ t_.line_string(:path)
191
+ t_.geometry(:shape)
192
+ end
193
+ klass_.connection.change_table(:spatial_test) do |t_|
194
+ t_.index(:latlon, :spatial => true)
195
+ end
196
+ klass_.class_eval do
197
+ self.rgeo_factory_generator = ::RGeo::Geos.method(:factory)
198
+ set_rgeo_factory_for_column(:latlon, ::RGeo::Geographic.spherical_factory)
199
+ end
200
+ rec_ = klass_.new
201
+ rec_.latlon = 'POINT(-122 47)'
202
+ loc_ = rec_.latlon
203
+ assert_equal(47, loc_.latitude)
204
+ rec_.shape = loc_
205
+ assert_equal(true, ::RGeo::Geos.is_geos?(rec_.shape))
206
+ end
207
+
208
+
209
+ end
210
+
211
+ end
212
+
213
+ end
214
+ end
215
+ end
216
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-spatialite-adapter
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Daniel Azuma
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-07 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rgeo-activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 2
31
+ - 0
32
+ version: 0.2.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: sqlite3-ruby
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 1
45
+ - 3
46
+ - 2
47
+ version: 1.3.2
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ description: This is an ActiveRecord connection adapter for the SpatiaLite extension to the Sqlite3 database. It is based on the stock sqlite3 adapter, but provides built-in support for spatial databases using SpatiaLite. It uses the RGeo library to represent spatial data in Ruby.
51
+ email: dazuma@gmail.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files:
57
+ - History.rdoc
58
+ - README.rdoc
59
+ files:
60
+ - lib/active_record/connection_adapters/spatialite_adapter.rb
61
+ - History.rdoc
62
+ - README.rdoc
63
+ - test/tc_basic.rb
64
+ - Version
65
+ has_rdoc: true
66
+ homepage: http://virtuoso.rubyforge.org/activerecord-spatialite-adapter
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options: []
71
+
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 1
81
+ - 8
82
+ - 7
83
+ version: 1.8.7
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project: virtuoso
95
+ rubygems_version: 1.3.7
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: An ActiveRecord adapter for SpatiaLite, based on RGeo.
99
+ test_files:
100
+ - test/tc_basic.rb