activerecord-spatialite-adapter 0.2.0

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