activerecord-postgis-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.
data/History.rdoc ADDED
@@ -0,0 +1,6 @@
1
+ === 0.2.0 / 2010-12-07
2
+
3
+ * Initial public alpha release. Spun activerecord-postgis-adapter off from the core rgeo gem.
4
+ * You can now set the factory for a specific column by name.
5
+
6
+ For earlier history, see the History file for the rgeo gem.
data/README.rdoc ADDED
@@ -0,0 +1,152 @@
1
+ == PostGIS \ActiveRecord Adapter
2
+
3
+ The PostGIS \ActiveRecord Adapter is an \ActiveRecord connection adapter
4
+ based on the standard postgresql adapter. It extends the standard adapter
5
+ to provide support for the spatial extensions provided by PostGIS, using
6
+ the {RGeo}[http://github.com/dazuma/rgeo] library to represent spatial
7
+ data in Ruby. Like the standard postgresql adapter, this adapter requires
8
+ the pg gem.
9
+
10
+ === Usage
11
+
12
+ To use this adapter, add this gem, "activerecord-postgis-adapter", to
13
+ your Gemfile, and then request the adapter name "postgis" in your
14
+ database connection configuration (which, for a Rails application, is in
15
+ the config/database.yml file). The other database connection configuration
16
+ parameters are the same as for the stock postgresql adapter.
17
+
18
+ First, this adapter extends the migration syntax to support creating
19
+ spatial columns and indexes. To create a spatial column, use the
20
+ <tt>:geometry</tt> type, or any of the OGC spatial types such as
21
+ <tt>:point</tt> or <tt>:line_string</tt>. To create a geography column,
22
+ set the <tt>:geographic</tt> option to true when creating the column.
23
+ To create a spatial index, set the <tt>:spatial</tt> option to true when
24
+ creating the index.
25
+
26
+ Examples:
27
+
28
+ create_table :spatial_table do |t|
29
+ t.column :shape, :geometry # or t.geometry :shape
30
+ t.line_string :path
31
+ t.column :latlon, :point, :geographic => true
32
+ end
33
+ change_table :spatial_table do |t|
34
+ t.index :latlon, :spatial => true
35
+ end
36
+
37
+ When this adapter is in use, spatial attributes in your \ActiveRecord
38
+ objects will have RGeo geometry values. You can set spatial attributes
39
+ either to RGeo geometry objects, or to strings in WKT (well-known text)
40
+ format, which the adapter will automatically convert to geometry objects.
41
+
42
+ To specify the RGeo geometry factory, you can either set an explicit
43
+ factory for a column, or provide a factory generator that will yield the
44
+ appropriate factory for the table's spatial columns based on their types.
45
+ For the former, call the set_rgeo_factory_for_column class method on your
46
+ \ActiveRecord class. For the latter, set the rgeo_factory_generator class
47
+ attribute. This generator should understand the usual <tt>:srid</tt>,
48
+ <tt>:has_z_coordinate</tt>, and <tt>:has_m_coordinate</tt> options. It
49
+ will also be passed a <tt>:geographic</tt> option indicating whether the
50
+ column is a geography column. The set_rgeo_factory_for_column and
51
+ rgeo_factory_generator methods are actually implemented and documented in
52
+ the "rgeo-activerecord" gem.
53
+
54
+ Examples, given the spatial table defined above:
55
+
56
+ class SpatialTable < ActiveRecord::Base
57
+
58
+ # By default, use the GEOS implementation for spatial columns.
59
+ self.rgeo_factory_generator = RGeo::Geos.method(:factory)
60
+
61
+ # But use a geographic implementation for the :latlon column.
62
+ set_rgeo_factory_for_column(:latlon, RGeo::Geographic.spherical_factory)
63
+
64
+ end
65
+
66
+ Now you can interact with the data using the RGeo types:
67
+
68
+ rec = SpatialTable.new
69
+ rec.latlon = 'POINT(-122 47)' # You can set by feature object or WKT.
70
+ loc = rec.latlon # Accessing always returns a feature object, in
71
+ # this case, a geographic that understands latitude.
72
+ loc.latitude # => 47
73
+ rec.shape = loc # the factory for the :shape column is GEOS, so the
74
+ # value will be cast from geographic to GEOS.
75
+ RGeo::Geos.is_geos?(rec.shape) # => true
76
+
77
+ === Installation
78
+
79
+ This adapter has the following requirements:
80
+
81
+ * Ruby 1.8.7 or later. Ruby 1.9.2 or later preferred.
82
+ * PostGIS 1.5 or later.
83
+ * \ActiveRecord 3.0.3 or later. Earlier versions will not work.
84
+ * rgeo gem 0.2.0 or later.
85
+ * rgeo-activerecord gem 0.2.0 or later.
86
+ * pg gem 0.10 or later.
87
+
88
+ Install this adapter as a gem:
89
+
90
+ gem install activerecord-postgis-adapter
91
+
92
+ See the README for the "rgeo" gem, a required dependency, for further
93
+ installation information.
94
+
95
+ === Known bugs and limitations
96
+
97
+ A few minor holes still exist in this adapter. Notably, using \ActiveRecord
98
+ to list indexes doesn't properly identify GiST (spatial) indexes as such.
99
+ This adapter is also not yet well tested.
100
+
101
+ === Development and support
102
+
103
+ Documentation is available at http://virtuoso.rubyforge.org/activerecord-postgis-adapter/README_rdoc.html
104
+
105
+ Source code is hosted on Github at http://github.com/dazuma/activerecord-postgis-adapter
106
+
107
+ Contributions are welcome. Fork the project on Github.
108
+
109
+ Report bugs on Github issues at http://github.org/dazuma/activerecord-postgis-adapter/issues
110
+
111
+ Contact the author at dazuma at gmail dot com.
112
+
113
+ === Acknowledgments
114
+
115
+ RGeo is written by Daniel Azuma (http://www.daniel-azuma.com).
116
+
117
+ Development of RGeo is sponsored by GeoPage, Inc. (http://www.geopage.com).
118
+
119
+ This adapter implementation owes some debt to the spatial_adapter plugin
120
+ (http://github.com/fragility/spatial_adapter). Although we made some
121
+ different design decisions for this adapter, studying the spatial_adapter
122
+ source gave us a head start on the implementation.
123
+
124
+ === License
125
+
126
+ Copyright 2010 Daniel Azuma
127
+
128
+ All rights reserved.
129
+
130
+ Redistribution and use in source and binary forms, with or without
131
+ modification, are permitted provided that the following conditions are met:
132
+
133
+ * Redistributions of source code must retain the above copyright notice,
134
+ this list of conditions and the following disclaimer.
135
+ * Redistributions in binary form must reproduce the above copyright notice,
136
+ this list of conditions and the following disclaimer in the documentation
137
+ and/or other materials provided with the distribution.
138
+ * Neither the name of the copyright holder, nor the names of any other
139
+ contributors to this software, may be used to endorse or promote products
140
+ derived from this software without specific prior written permission.
141
+
142
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
143
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
144
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
145
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
146
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
147
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
148
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
149
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
150
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
151
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
152
+ POSSIBILITY OF SUCH DAMAGE.
data/Version ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,444 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # PostGIS 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/postgresql_adapter'
39
+
40
+
41
+ # :stopdoc:
42
+
43
+ module Arel
44
+ module Visitors
45
+ VISITORS['postgis'] = ::Arel::Visitors::PostgreSQL
46
+ end
47
+ end
48
+
49
+ # :startdoc:
50
+
51
+
52
+ # The activerecord-postgis-adapter gem installs the *postgis*
53
+ # connection adapter into ActiveRecord.
54
+
55
+ module ActiveRecord
56
+
57
+
58
+ # ActiveRecord looks for the postgis_connection factory method in
59
+ # this class.
60
+
61
+ class Base
62
+
63
+
64
+ # Create a postgis connection adapter.
65
+
66
+ def self.postgis_connection(config_)
67
+ require 'pg'
68
+
69
+ config_ = config_.symbolize_keys
70
+ host_ = config_[:host]
71
+ port_ = config_[:port] || 5432
72
+ username_ = config_[:username].to_s if config_[:username]
73
+ password_ = config_[:password].to_s if config_[:password]
74
+ if config_.has_key?(:database)
75
+ database_ = config_[:database]
76
+ else
77
+ raise ::ArgumentError, "No database specified. Missing argument: database."
78
+ end
79
+
80
+ # The postgres drivers don't allow the creation of an unconnected PGconn object,
81
+ # so just pass a nil connection object for the time being.
82
+ ConnectionAdapters::PostGisAdapter.new(nil, logger, [host_, port_, nil, nil, database_, username_, password_], config_)
83
+ end
84
+
85
+
86
+ end
87
+
88
+
89
+ module ConnectionAdapters # :nodoc:
90
+
91
+
92
+ class PostGisAdapter < PostgreSQLAdapter # :nodoc:
93
+
94
+
95
+ ADAPTER_NAME = 'PostGIS'.freeze
96
+
97
+ @@native_database_types = nil
98
+
99
+
100
+ def native_database_types
101
+ @@native_database_types ||= super.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"})
102
+ end
103
+
104
+
105
+ def adapter_name
106
+ ADAPTER_NAME
107
+ end
108
+
109
+
110
+ def postgis_lib_version
111
+ unless defined?(@postgis_lib_version)
112
+ @postgis_lib_version = select_value("SELECT PostGIS_Lib_Version()") rescue nil
113
+ end
114
+ @postgis_lib_version
115
+ end
116
+
117
+
118
+ def quote(value_, column_=nil)
119
+ if ::RGeo::Feature::Geometry.check_type(value_)
120
+ "'#{::RGeo::WKRep::WKBGenerator.new(:hex_format => true, :type_format => :ewkb, :emit_ewkb_srid => true).generate(value_)}'"
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+
127
+ def columns(table_name_, name_=nil) #:nodoc:
128
+ table_name_ = table_name_.to_s
129
+ spatial_info_ = spatial_column_info(table_name_)
130
+ column_definitions(table_name_).collect do |name_, type_, default_, notnull_|
131
+ SpatialColumn.new(name_, default_, type_, notnull_ == 'f', type_ =~ /geometry/i ? spatial_info_[name_] : nil)
132
+ end
133
+ end
134
+
135
+
136
+ def create_table(table_name_, options_={})
137
+ table_name_ = table_name_.to_s
138
+ table_definition_ = SpatialTableDefinition.new(self)
139
+ table_definition_.primary_key(options_[:primary_key] || ::ActiveRecord::Base.get_primary_key(table_name_.singularize)) unless options_[:id] == false
140
+ yield table_definition_ if block_given?
141
+ if options_[:force] && table_exists?(table_name_)
142
+ drop_table(table_name_, options_)
143
+ end
144
+
145
+ create_sql_ = "CREATE#{' TEMPORARY' if options_[:temporary]} TABLE "
146
+ create_sql_ << "#{quote_table_name(table_name_)} ("
147
+ create_sql_ << table_definition_.to_sql
148
+ create_sql_ << ") #{options_[:options]}"
149
+ execute create_sql_
150
+
151
+ table_definition_.non_geographic_spatial_columns.each do |col_|
152
+ type_ = col_.type.to_s.gsub('_', '').upcase
153
+ has_z_ = col_.has_z?
154
+ has_m_ = col_.has_m?
155
+ type_ = "#{type_}M" if has_m_ && !has_z_
156
+ dimensions_ = 2
157
+ dimensions_ += 1 if has_z_
158
+ dimensions_ += 1 if has_m_
159
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_)}', '#{quote_string(col_.name)}', #{col_.srid}, '#{quote_string(type_)}', #{dimensions_})")
160
+ end
161
+ end
162
+
163
+
164
+ def drop_table(table_name_, options_={})
165
+ execute("DELETE from geometry_columns where f_table_name='#{quote_string(table_name_.to_s)}'")
166
+ super
167
+ end
168
+
169
+
170
+ def add_column(table_name_, column_name_, type_, options_={})
171
+ table_name_ = table_name_.to_s
172
+ if ::RGeo::ActiveRecord::GEOMETRY_TYPES.include?(type_.to_sym)
173
+ type_ = type_.to_s.gsub('_', '').upcase
174
+ has_z_ = options_[:has_z]
175
+ has_m_ = options_[:has_m]
176
+ srid_ = (options_[:srid] || 4326).to_i
177
+ if options_[:geographic]
178
+ type_ << 'Z' if has_z_
179
+ type_ << 'M' if has_m_
180
+ execute("ALTER TABLE #{quote_table_name(table_name_)} ADD COLUMN #{quote_column_name(column_name_)} GEOGRAPHY(#{type_},#{srid_})")
181
+ change_column_default(table_name_, column_name_, options_[:default]) if options_include_default?(options_)
182
+ change_column_null(table_name_, column_name_, false, options_[:default]) if options_[:null] == false
183
+ else
184
+ type_ = "#{type_}M" if has_m_ && !has_z_
185
+ dimensions_ = 2
186
+ dimensions_ += 1 if has_z_
187
+ dimensions_ += 1 if has_m_
188
+ execute("SELECT AddGeometryColumn('#{quote_string(table_name_)}', '#{quote_string(column_name_.to_s)}', #{srid_}, '#{quote_string(type_)}', #{dimensions_})")
189
+ end
190
+ else
191
+ super
192
+ end
193
+ end
194
+
195
+
196
+ def remove_column(table_name_, *column_names_)
197
+ column_names_ = column_names_.flatten.map{ |n_| n_.to_s }
198
+ spatial_info_ = spatial_column_info(table_name_)
199
+ remaining_column_names_ = []
200
+ column_names_.each do |name_|
201
+ if spatial_info_.include?(name_)
202
+ execute("SELECT DropGeometryColumn('#{quote_string(table_name_.to_s)}','#{quote_string(name_)}')")
203
+ else
204
+ remaining_column_names_ << name_.to_sym
205
+ end
206
+ end
207
+ if remaining_column_names_.size > 0
208
+ super(table_name_, *remaining_column_names_)
209
+ end
210
+ end
211
+
212
+
213
+ def add_index(table_name_, column_name_, options_={})
214
+ table_name_ = table_name_.to_s
215
+ column_names_ = ::Array.wrap(column_name_)
216
+ index_name_ = index_name(table_name_, :column => column_names_)
217
+ gist_clause_ = ''
218
+ index_type_ = ''
219
+ if ::Hash === options_ # legacy support, since this param was a string
220
+ index_type_ = 'UNIQUE' if options_[:unique]
221
+ index_name_ = options_[:name].to_s if options_.key?(:name)
222
+ gist_clause_ = 'USING GIST' if options_[:spatial]
223
+ else
224
+ index_type_ = options_
225
+ end
226
+ if index_name_.length > index_name_length
227
+ raise ::ArgumentError, "Index name '#{index_name_}' on table '#{table_name_}' is too long; the limit is #{index_name_length} characters"
228
+ end
229
+ if index_name_exists?(table_name_, index_name_, false)
230
+ raise ::ArgumentError, "Index name '#{index_name_}' on table '#{table_name_}' already exists"
231
+ end
232
+ quoted_column_names_ = quoted_columns_for_index(column_names_, options_).join(", ")
233
+ execute "CREATE #{index_type_} INDEX #{quote_column_name(index_name_)} ON #{quote_table_name(table_name_)} #{gist_clause_} (#{quoted_column_names_})"
234
+ end
235
+
236
+
237
+ def spatial_column_info(table_name_)
238
+ info_ = query("SELECT * FROM geometry_columns WHERE f_table_name='#{quote_string(table_name_.to_s)}'")
239
+ result_ = {}
240
+ info_.each do |row_|
241
+ name_ = row_[3]
242
+ type_ = row_[6]
243
+ dimension_ = row_[4].to_i
244
+ has_m_ = type_ =~ /m$/i ? true : false
245
+ type_.sub!(/m$/, '')
246
+ has_z_ = dimension_ > 3 || dimension_ == 3 && !has_m_
247
+ result_[name_] = {
248
+ :name => name_,
249
+ :type => type_,
250
+ :dimension => dimension_,
251
+ :srid => row_[5].to_i,
252
+ :has_z => has_z_,
253
+ :has_m => has_m_,
254
+ }
255
+ end
256
+ result_
257
+ end
258
+
259
+
260
+ class SpatialTableDefinition < ConnectionAdapters::TableDefinition # :nodoc:
261
+
262
+ attr_reader :spatial_columns
263
+
264
+ def initialize(base_)
265
+ super
266
+ end
267
+
268
+ def column(name_, type_, options_={})
269
+ super
270
+ col_ = self[name_]
271
+ if ::RGeo::ActiveRecord::GEOMETRY_TYPES.include?(col_.type.to_sym)
272
+ col_.extend(GeometricColumnDefinitionMethods) unless col_.respond_to?(:geographic?)
273
+ col_.set_geographic(options_[:geographic])
274
+ col_.set_srid((options_[:srid] || 4326).to_i)
275
+ col_.set_has_z(options_[:has_z])
276
+ col_.set_has_m(options_[:has_m])
277
+ end
278
+ self
279
+ end
280
+
281
+ def to_sql
282
+ @columns.find_all{ |c_| !c_.respond_to?(:geographic?) || c_.geographic? }.map{ |c_| c_.to_sql } * ', '
283
+ end
284
+
285
+ def non_geographic_spatial_columns
286
+ @columns.find_all{ |c_| c_.respond_to?(:geographic?) && !c_.geographic? }
287
+ end
288
+
289
+ end
290
+
291
+
292
+ module GeometricColumnDefinitionMethods # :nodoc:
293
+
294
+ def geographic?
295
+ defined?(@geographic) && @geographic
296
+ end
297
+
298
+ def srid
299
+ defined?(@srid) ? @srid : 4326
300
+ end
301
+
302
+ def has_z?
303
+ defined?(@has_z) && @has_z
304
+ end
305
+
306
+ def has_m?
307
+ defined?(@has_m) && @has_m
308
+ end
309
+
310
+ def set_geographic(value_)
311
+ @geographic = value_ ? true : false
312
+ end
313
+
314
+ def set_srid(value_)
315
+ @srid = value_
316
+ end
317
+
318
+ def set_has_z(value_)
319
+ @has_z = value_ ? true : false
320
+ end
321
+
322
+ def set_has_m(value_)
323
+ @has_m = value_ ? true : false
324
+ end
325
+
326
+ def sql_type
327
+ type_ = type.to_s.upcase.gsub('_', '')
328
+ type_ << 'Z' if has_z?
329
+ type_ << 'M' if has_m?
330
+ "GEOGRAPHY(#{type_},#{srid})"
331
+ end
332
+
333
+ end
334
+
335
+
336
+ class SpatialColumn < ConnectionAdapters::PostgreSQLColumn # :nodoc:
337
+
338
+
339
+ def initialize(name_, default_, sql_type_=nil, null_=true, opts_=nil)
340
+ super(name_, default_, sql_type_, null_)
341
+ @geographic = sql_type_ =~ /^geography/ ? true : false
342
+ if opts_
343
+ @geometric_type = ::RGeo::ActiveRecord.geometric_type_from_name(opts_[:type])
344
+ @srid = opts_[:srid].to_i
345
+ @has_z = opts_[:has_z]
346
+ @has_m = opts_[:has_m]
347
+ elsif @geographic
348
+ if sql_type_ =~ /geography\((\w+[^,zm])(z?)(m?),(\d+)\)/i
349
+ @has_z = $2.length > 0
350
+ @has_m = $3.length > 0
351
+ @srid = $4.to_i
352
+ @geometric_type = ::RGeo::ActiveRecord.geometric_type_from_name($1)
353
+ else
354
+ @geometric_type = ::RGeo::Feature::Geometry
355
+ @srid = 4326
356
+ @has_z = @has_m = false
357
+ end
358
+ else
359
+ @geometric_type = @has_z = @has_m = nil
360
+ @srid = 0
361
+ end
362
+ @ar_class = ::ActiveRecord::Base
363
+ end
364
+
365
+
366
+ def set_ar_class(val_)
367
+ @ar_class = val_
368
+ end
369
+
370
+
371
+ attr_reader :srid
372
+ attr_reader :geometric_type
373
+ attr_reader :has_z
374
+ attr_reader :has_m
375
+
376
+
377
+ def spatial?
378
+ type == :geometry
379
+ end
380
+
381
+
382
+ def geographic?
383
+ @geographic
384
+ end
385
+
386
+
387
+ def klass
388
+ type == :geometry ? ::RGeo::Feature::Geometry : super
389
+ end
390
+
391
+
392
+ def type_cast(value_)
393
+ type == :geometry ? SpatialColumn.convert_to_geometry(value_, @ar_class, name, @geographic, @srid, @has_z, @has_m) : super
394
+ end
395
+
396
+
397
+ def type_cast_code(var_name_)
398
+ type == :geometry ? "::ActiveRecord::ConnectionAdapters::PostGisAdapter::SpatialColumn.convert_to_geometry(#{var_name_}, self.class, #{name.inspect}, #{@geographic ? 'true' : 'false'}, #{@srid.inspect}, #{@has_z ? 'true' : 'false'}, #{@has_m ? 'true' : 'false'})" : super
399
+ end
400
+
401
+
402
+ private
403
+
404
+
405
+ def simplified_type(sql_type_)
406
+ sql_type_ =~ /geography|geometry|point|linestring|polygon/i ? :geometry : super
407
+ end
408
+
409
+
410
+ def self.convert_to_geometry(input_, ar_class_, column_, geographic_, srid_, has_z_, has_m_)
411
+ case input_
412
+ when ::RGeo::Feature::Geometry
413
+ factory_ = ar_class_.rgeo_factory_for_column(column_, :srid => srid_, :has_z_coordinate => has_z_, :has_m_coordinate => has_m_, :geographic => geographic_)
414
+ ::RGeo::Feature.cast(input_, factory_)
415
+ when ::String
416
+ if input_.length == 0
417
+ nil
418
+ else
419
+ factory_ = ar_class_.rgeo_factory_for_column(column_, :srid => srid_, :has_z_coordinate => has_z_, :has_m_coordinate => has_m_, :geographic => geographic_)
420
+ marker_ = input_[0,1]
421
+ if marker_ == "\x00" || marker_ == "\x01"
422
+ ::RGeo::WKRep::WKBParser.new(factory_, :support_ewkb => true).parse(input_) rescue nil
423
+ elsif input_[0,4] =~ /[0-9a-fA-F]{4}/
424
+ ::RGeo::WKRep::WKBParser.new(factory_, :support_ewkb => true).parse_hex(input_) rescue nil
425
+ else
426
+ ::RGeo::WKRep::WKTParser.new(factory_, :support_ewkt => true).parse(input_) rescue nil
427
+ end
428
+ end
429
+ else
430
+ nil
431
+ end
432
+ end
433
+
434
+
435
+ end
436
+
437
+
438
+ end
439
+
440
+
441
+ end
442
+
443
+
444
+ end
data/test/tc_basic.rb ADDED
@@ -0,0 +1,271 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Tests for the PostGIS 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 PostGISAdapter # :nodoc:
43
+ module Tests # :nodoc:
44
+
45
+ class TestBasic < ::Test::Unit::TestCase # :nodoc:
46
+
47
+ DATABASE_CONFIG_PATH = ::File.dirname(__FILE__)+'/database.yml'
48
+ include AdapterTestHelper
49
+
50
+ define_test_methods do
51
+
52
+
53
+ def populate_ar_class(content_)
54
+ klass_ = create_ar_class
55
+ case content_
56
+ when :latlon_point
57
+ klass_.connection.create_table(:spatial_test) do |t_|
58
+ t_.column 'latlon', :point, :srid => 4326
59
+ end
60
+ when :latlon_point_geographic
61
+ klass_.connection.create_table(:spatial_test) do |t_|
62
+ t_.column 'latlon', :point, :srid => 4326, :geographic => true
63
+ end
64
+ end
65
+ klass_
66
+ end
67
+
68
+
69
+ def test_postgis_available
70
+ connection_ = create_ar_class.connection
71
+ assert_equal('PostGIS', connection_.adapter_name)
72
+ assert_not_nil(connection_.postgis_lib_version)
73
+ end
74
+
75
+
76
+ def test_create_simple_geometry
77
+ klass_ = create_ar_class
78
+ klass_.connection.create_table(:spatial_test) do |t_|
79
+ t_.column 'latlon', :geometry
80
+ end
81
+ assert_equal(1, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
82
+ col_ = klass_.columns.last
83
+ assert_equal(::RGeo::Feature::Geometry, col_.geometric_type)
84
+ assert_equal(false, col_.geographic?)
85
+ assert_equal(4326, col_.srid)
86
+ assert(klass_.cached_attributes.include?('latlon'))
87
+ klass_.connection.drop_table(:spatial_test)
88
+ assert_equal(0, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
89
+ end
90
+
91
+
92
+ def test_create_simple_geography
93
+ klass_ = create_ar_class
94
+ klass_.connection.create_table(:spatial_test) do |t_|
95
+ t_.column 'latlon', :geometry, :geographic => true
96
+ end
97
+ col_ = klass_.columns.last
98
+ assert_equal(::RGeo::Feature::Geometry, col_.geometric_type)
99
+ assert_equal(true, col_.geographic?)
100
+ assert_equal(4326, col_.srid)
101
+ assert(klass_.cached_attributes.include?('latlon'))
102
+ assert_equal(0, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
103
+ end
104
+
105
+
106
+ def test_create_point_geometry
107
+ klass_ = create_ar_class
108
+ klass_.connection.create_table(:spatial_test) do |t_|
109
+ t_.column 'latlon', :point
110
+ end
111
+ assert_equal(::RGeo::Feature::Point, klass_.columns.last.geometric_type)
112
+ assert(klass_.cached_attributes.include?('latlon'))
113
+ end
114
+
115
+
116
+ def test_create_geometry_with_index
117
+ klass_ = create_ar_class
118
+ klass_.connection.create_table(:spatial_test) do |t_|
119
+ t_.column 'latlon', :geometry
120
+ end
121
+ klass_.connection.change_table(:spatial_test) do |t_|
122
+ t_.index([:latlon], :spatial => true)
123
+ end
124
+ end
125
+
126
+
127
+ def test_set_and_get_point
128
+ klass_ = populate_ar_class(:latlon_point)
129
+ obj_ = klass_.new
130
+ assert_nil(obj_.latlon)
131
+ obj_.latlon = @factory.point(1, 2)
132
+ assert_equal(@factory.point(1, 2), obj_.latlon)
133
+ assert_equal(4326, obj_.latlon.srid)
134
+ end
135
+
136
+
137
+ def test_set_and_get_point_from_wkt
138
+ klass_ = populate_ar_class(:latlon_point)
139
+ obj_ = klass_.new
140
+ assert_nil(obj_.latlon)
141
+ obj_.latlon = 'POINT(1 2)'
142
+ assert_equal(@factory.point(1, 2), obj_.latlon)
143
+ assert_equal(4326, obj_.latlon.srid)
144
+ end
145
+
146
+
147
+ def test_save_and_load_point
148
+ klass_ = populate_ar_class(:latlon_point)
149
+ obj_ = klass_.new
150
+ obj_.latlon = @factory.point(1, 2)
151
+ obj_.save!
152
+ id_ = obj_.id
153
+ obj2_ = klass_.find(id_)
154
+ assert_equal(@factory.point(1, 2), obj2_.latlon)
155
+ assert_equal(4326, obj2_.latlon.srid)
156
+ assert_equal(true, ::RGeo::Geos.is_geos?(obj2_.latlon))
157
+ end
158
+
159
+
160
+ def test_save_and_load_geographic_point
161
+ klass_ = populate_ar_class(:latlon_point_geographic)
162
+ obj_ = klass_.new
163
+ obj_.latlon = @factory.point(1, 2)
164
+ obj_.save!
165
+ id_ = obj_.id
166
+ obj2_ = klass_.find(id_)
167
+ assert_equal(@geographic_factory.point(1, 2), obj2_.latlon)
168
+ assert_equal(4326, obj2_.latlon.srid)
169
+ assert_equal(false, ::RGeo::Geos.is_geos?(obj2_.latlon))
170
+ end
171
+
172
+
173
+ def test_save_and_load_point_from_wkt
174
+ klass_ = populate_ar_class(:latlon_point)
175
+ obj_ = klass_.new
176
+ obj_.latlon = 'POINT(1 2)'
177
+ obj_.save!
178
+ id_ = obj_.id
179
+ obj2_ = klass_.find(id_)
180
+ assert_equal(@factory.point(1, 2), obj2_.latlon)
181
+ assert_equal(4326, obj2_.latlon.srid)
182
+ end
183
+
184
+
185
+ def test_add_geometry_column
186
+ klass_ = create_ar_class
187
+ klass_.connection.create_table(:spatial_test) do |t_|
188
+ t_.column('latlon', :geometry)
189
+ end
190
+ klass_.connection.change_table(:spatial_test) do |t_|
191
+ t_.column('geom2', :point, :srid => 4326)
192
+ t_.column('name', :string)
193
+ end
194
+ assert_equal(2, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
195
+ cols_ = klass_.columns
196
+ assert_equal(::RGeo::Feature::Geometry, cols_[-3].geometric_type)
197
+ assert_equal(4326, cols_[-3].srid)
198
+ assert_equal(::RGeo::Feature::Point, cols_[-2].geometric_type)
199
+ assert_equal(4326, cols_[-2].srid)
200
+ assert_equal(false, cols_[-2].geographic?)
201
+ assert_nil(cols_[-1].geometric_type)
202
+ end
203
+
204
+
205
+ def test_add_geography_column
206
+ klass_ = create_ar_class
207
+ klass_.connection.create_table(:spatial_test) do |t_|
208
+ t_.column('latlon', :geometry)
209
+ end
210
+ klass_.connection.change_table(:spatial_test) do |t_|
211
+ t_.column('geom2', :point, :srid => 4326, :geographic => true)
212
+ t_.column('name', :string)
213
+ end
214
+ assert_equal(1, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
215
+ cols_ = klass_.columns
216
+ assert_equal(::RGeo::Feature::Geometry, cols_[-3].geometric_type)
217
+ assert_equal(4326, cols_[-3].srid)
218
+ assert_equal(::RGeo::Feature::Point, cols_[-2].geometric_type)
219
+ assert_equal(4326, cols_[-2].srid)
220
+ assert_equal(true, cols_[-2].geographic?)
221
+ assert_nil(cols_[-1].geometric_type)
222
+ end
223
+
224
+
225
+ def test_drop_geometry_column
226
+ klass_ = create_ar_class
227
+ klass_.connection.create_table(:spatial_test) do |t_|
228
+ t_.column('latlon', :geometry)
229
+ t_.column('geom2', :point, :srid => 4326)
230
+ end
231
+ klass_.connection.change_table(:spatial_test) do |t_|
232
+ t_.remove('geom2')
233
+ end
234
+ assert_equal(1, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
235
+ cols_ = klass_.columns
236
+ assert_equal(::RGeo::Feature::Geometry, cols_[-1].geometric_type)
237
+ assert_equal('latlon', cols_[-1].name)
238
+ assert_equal(4326, cols_[-1].srid)
239
+ assert_equal(false, cols_[-1].geographic?)
240
+ end
241
+
242
+
243
+ def test_drop_geography_column
244
+ klass_ = create_ar_class
245
+ klass_.connection.create_table(:spatial_test) do |t_|
246
+ t_.column('latlon', :geometry)
247
+ t_.column('geom2', :point, :srid => 4326, :geographic => true)
248
+ t_.column('geom3', :point, :srid => 4326)
249
+ end
250
+ klass_.connection.change_table(:spatial_test) do |t_|
251
+ t_.remove('geom2')
252
+ end
253
+ assert_equal(2, klass_.connection.select_value("SELECT COUNT(*) FROM geometry_columns WHERE f_table_name='spatial_test'").to_i)
254
+ cols_ = klass_.columns
255
+ assert_equal(::RGeo::Feature::Point, cols_[-1].geometric_type)
256
+ assert_equal('geom3', cols_[-1].name)
257
+ assert_equal(false, cols_[-1].geographic?)
258
+ assert_equal(::RGeo::Feature::Geometry, cols_[-2].geometric_type)
259
+ assert_equal('latlon', cols_[-2].name)
260
+ assert_equal(false, cols_[-2].geographic?)
261
+ end
262
+
263
+
264
+ end
265
+
266
+ end
267
+
268
+ end
269
+ end
270
+ end
271
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-postgis-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: pg
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 0
45
+ - 10
46
+ - 0
47
+ version: 0.10.0
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ description: This is an ActiveRecord connection adapter for PostGIS. It is based on the stock PostgreSQL adapter, but provides built-in support for the spatial extensions provided by PostGIS. 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/postgis_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-postgis-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 PostGIS, based on RGeo.
99
+ test_files:
100
+ - test/tc_basic.rb