activerecord-postgis-adapter 0.2.0

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