geos-extensions 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.2.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "geos-extensions"
8
- s.version = "0.1.6"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["J Smith"]
12
- s.date = "2012-02-28"
12
+ s.date = "2012-05-23"
13
13
  s.description = "Extensions for the GEOS library."
14
14
  s.email = "code@zoocasa.com"
15
15
  s.extra_rdoc_files = [
@@ -26,8 +26,8 @@ Gem::Specification.new do |s|
26
26
  "lib/geos-extensions.rb",
27
27
  "lib/geos/active_record_extensions.rb",
28
28
  "lib/geos/active_record_extensions/connection_adapters/postgresql_adapter.rb",
29
- "lib/geos/active_record_extensions/geometry_columns.rb",
30
- "lib/geos/active_record_extensions/geospatial_scopes.rb",
29
+ "lib/geos/active_record_extensions/spatial_columns.rb",
30
+ "lib/geos/active_record_extensions/spatial_scopes.rb",
31
31
  "lib/geos/geos_helper.rb",
32
32
  "lib/geos/google_maps.rb",
33
33
  "lib/geos/google_maps/api_2.rb",
@@ -38,20 +38,24 @@ Gem::Specification.new do |s|
38
38
  "lib/tasks/test.rake",
39
39
  "test/adapter_tests.rb",
40
40
  "test/database.yml",
41
+ "test/fixtures/foo3ds.yml",
42
+ "test/fixtures/foo_geographies.yml",
41
43
  "test/fixtures/foos.yml",
44
+ "test/geography_columns_tests.rb",
42
45
  "test/geometry_columns_tests.rb",
43
- "test/geospatial_scopes_tests.rb",
44
46
  "test/google_maps_api_2_tests.rb",
45
47
  "test/google_maps_api_3_tests.rb",
46
48
  "test/google_maps_polyline_encoder_tests.rb",
47
49
  "test/misc_tests.rb",
48
50
  "test/reader_tests.rb",
51
+ "test/spatial_scopes_geographies_tests.rb",
52
+ "test/spatial_scopes_tests.rb",
49
53
  "test/test_helper.rb",
50
54
  "test/writer_tests.rb"
51
55
  ]
52
56
  s.homepage = "http://github.com/zoocasa/geos-extensions"
53
57
  s.require_paths = ["lib"]
54
- s.rubygems_version = "1.8.16"
58
+ s.rubygems_version = "1.8.24"
55
59
  s.summary = "Extensions for the GEOS library."
56
60
 
57
61
  if s.respond_to? :specification_version then
@@ -1,39 +1,61 @@
1
1
 
2
+ require 'active_record/connection_adapters/postgresql_adapter'
3
+
2
4
  module ActiveRecord
3
5
  module ConnectionAdapters
4
6
  # Allows access to the name, srid and coord_dimensions of a PostGIS
5
7
  # geometry column in PostgreSQL.
6
- class PostgreSQLGeometryColumn
7
- attr_accessor :name, :srid, :coord_dimension
8
+ class PostgreSQLSpatialColumn
9
+ attr_accessor :name, :srid, :coord_dimension, :spatial_type
10
+
11
+ def initialize(name, options = {})
12
+ options = {
13
+ :srid => nil,
14
+ :coord_dimension => nil,
15
+ :spatial_type => :geometry
16
+ }.merge(options)
8
17
 
9
- def initialize(name, srid = nil, coord_dimension = nil)
10
- @name, @srid, @coord_dimension = name, srid, coord_dimension
18
+ @name = name
19
+ @srid = options[:srid]
20
+ @coord_dimension = options[:coord_dimension]
21
+ @spatial_type = options[:spatial_type]
11
22
  end
12
23
  end
13
24
 
14
25
  class PostgreSQLColumn
15
- def simplified_type_with_geometry_type(field_type)
16
- if field_type == 'geometry'
26
+ def simplified_type_with_spatial_type(field_type)
27
+ if field_type =~ /^geometry(\(|$)/
17
28
  :geometry
29
+ elsif field_type =~ /^geography(\(|$)/
30
+ :geography
18
31
  else
19
- simplified_type_without_geometry_type(field_type)
32
+ simplified_type_without_spatial_type(field_type)
20
33
  end
21
34
  end
22
- alias_method_chain :simplified_type, :geometry_type
35
+ alias_method_chain :simplified_type, :spatial_type
23
36
  end
24
37
 
25
38
  class PostgreSQLAdapter
39
+ def geometry_columns?
40
+ true
41
+ end
42
+
43
+ def geography_columns?
44
+ Geos::ActiveRecord.POSTGIS[:lib] >= '1.5'
45
+ end
46
+
26
47
  # Returns the geometry columns for the table.
27
48
  def geometry_columns(table_name, name = nil)
28
- return [] if !table_exists?(table_name)
49
+ return [] if !geometry_columns? ||
50
+ !table_exists?(table_name)
29
51
 
30
- columns(table_name, name).select { |c| c.sql_type == 'geometry' }.collect do |c|
52
+ columns(table_name, name).select { |c| c.type == :geometry }.collect do |c|
31
53
  res = select_rows(
32
54
  "SELECT srid, coord_dimension FROM geometry_columns WHERE f_table_name = #{quote(table_name)} AND f_geometry_column = #{quote(c.name)}",
33
55
  "Geometry column load for #{table_name}"
34
56
  )
35
57
 
36
- PostgreSQLGeometryColumn.new(c.name).tap do |g|
58
+ PostgreSQLSpatialColumn.new(c.name).tap do |g|
37
59
  # since we're too stupid at the moment to understand
38
60
  # PostgreSQL schemas, let's just go with this:
39
61
  if res.length == 1
@@ -42,6 +64,88 @@ module ActiveRecord
42
64
  end
43
65
  end
44
66
  end
67
+
68
+ # Returns the geography columns for the table.
69
+ def geography_columns(table_name, name = nil)
70
+ return [] if !geography_columns? ||
71
+ !table_exists?(table_name)
72
+
73
+ columns(table_name, name).select { |c| c.type == :geography }.collect do |c|
74
+ res = select_rows(
75
+ "SELECT srid, coord_dimension FROM geography_columns WHERE f_table_name = #{quote(table_name)} AND f_geography_column = #{quote(c.name)}",
76
+ "Geography column load for #{table_name}"
77
+ )
78
+
79
+ PostgreSQLSpatialColumn.new(c.name, :spatial_type => :geography).tap do |g|
80
+ # since we're too stupid at the moment to understand
81
+ # PostgreSQL schemas, let's just go with this:
82
+ if res.length == 1
83
+ g.srid, g.coord_dimension = res.first.collect { |value|
84
+ value.try(:to_i)
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Returns both the geometry and geography columns for the table.
92
+ def spatial_columns(table_name, name = nil)
93
+ geometry_columns(table_name, name) +
94
+ geography_columns(table_name, name)
95
+ end
96
+ end
97
+
98
+ # Alias for backwards compatibility:
99
+ PostgreSQLGeometryColumn = PostgreSQLSpatialColumn
100
+ end
101
+ end
102
+
103
+ module Geos
104
+ module ActiveRecord
105
+ def self.POSTGIS
106
+ return @POSTGIS if defined?(@POSTGIS)
107
+
108
+ @POSTGIS = if (version_string = ::ActiveRecord::Base.connection.select_rows("SELECT postgis_full_version()").flatten.first).present?
109
+ hash = {
110
+ :use_stats => version_string =~ /USE_STATS/
111
+ }
112
+
113
+ {
114
+ :lib => /POSTGIS="([^"]+)"/,
115
+ :geos => /GEOS="([^"]+)"/,
116
+ :proj => /PROJ="([^"]+)"/,
117
+ :libxml => /LIBXML="([^"]+)"/
118
+ }.each do |k, v|
119
+ hash[k] = version_string.scan(v).flatten.first
120
+ end
121
+
122
+ hash.freeze
123
+ else
124
+ {}.freeze
125
+ end
126
+ end
127
+
128
+ def self.UNKNOWN_SRIDS
129
+ return @UNKNOWN_SRIDS if defined?(@UNKNOWN_SRIDS)
130
+
131
+ @UNKNOWN_SRIDS = if self.POSTGIS[:lib] >= '2.0'
132
+ {
133
+ :geography => 0,
134
+ :geometry => 0
135
+ }.freeze
136
+ else
137
+ {
138
+ :geography => 0,
139
+ :geometry => -1
140
+ }.freeze
141
+ end
142
+ end
143
+
144
+ def self.UNKNOWN_SRID
145
+ return @UNKNOWN_SRID if defined?(@UNKNOWN_SRID)
146
+
147
+ @UNKNOWN_SRID = self.UNKNOWN_SRIDS[:geometry]
45
148
  end
46
149
  end
47
150
  end
151
+
@@ -0,0 +1,369 @@
1
+
2
+ module Geos
3
+ module ActiveRecord #:nodoc:
4
+ class << self
5
+ def geometry_columns?
6
+ ::ActiveRecord::Base.connection.geometry_columns?
7
+ end
8
+
9
+ def geography_columns?
10
+ ::ActiveRecord::Base.connection.geography_columns?
11
+ end
12
+ end
13
+
14
+ # This little module helps us out with geometry columns. At least, in
15
+ # PostgreSQL it does.
16
+ #
17
+ # This module will add a method called geometry_columns to your model
18
+ # which will contain information that can be gleaned from the
19
+ # geometry_columns table that PostGIS creates.
20
+ #
21
+ # You can also have the module automagically create some accessor
22
+ # methods for you to make your life easier. These accessor methods will
23
+ # override the ActiveRecord defaults and allow you to set geometry
24
+ # column values using Geos geometry objects directly or with
25
+ # PostGIS-style extended WKT and such. See
26
+ # create_geometry_column_accessors! for details.
27
+ #
28
+ # === Caveats:
29
+ #
30
+ # * This module currently only works with PostGIS.
31
+ # * This module doesn't really "get" PostgreSQL catalogs and schemas
32
+ # and such. That would be a little more involved but it would be
33
+ # nice if Rails was aware of such things.
34
+ module SpatialColumns
35
+ SPATIAL_COLUMN_OUTPUT_FORMATS = %w{ geos wkt wkb ewkt ewkb wkb_bin ewkb_bin }.freeze
36
+
37
+ class InvalidGeometry < ::ActiveRecord::ActiveRecordError
38
+ def initialize(geom)
39
+ super("Invalid geometry: #{geom}")
40
+ end
41
+ end
42
+
43
+ class SRIDNotFound < ::ActiveRecord::ActiveRecordError
44
+ def initialize(table_name, column)
45
+ super("Couldn't find SRID for #{table_name}.#{column}")
46
+ end
47
+ end
48
+
49
+ class CantConvertSRID < ::ActiveRecord::ActiveRecordError
50
+ def initialize(table_name, column, from_srid, to_srid)
51
+ super("Couldn't convert SRID for #{table_name}.#{column} from #{from_srid} to #{to_srid}")
52
+ end
53
+ end
54
+
55
+ def self.included(base) #:nodoc:
56
+ base.extend(ClassMethods)
57
+ base.send(:include, Geos::ActiveRecord::SpatialScopes)
58
+ end
59
+
60
+ module ClassMethods
61
+ protected
62
+ @geometry_columns = nil
63
+ @geography_columns = nil
64
+
65
+ public
66
+ # Stubs for documentation purposes:
67
+
68
+ # Returns an Array of available geometry columns in the
69
+ # table. These are PostgreSQLColumns with values set for
70
+ # the srid and coord_dimensions properties.
71
+ def geometry_columns; end
72
+
73
+ # Returns an Array of available geography columns in the
74
+ # table. These are PostgreSQLColumns with values set for
75
+ # the srid and coord_dimensions properties.
76
+ def geography_columns; end
77
+
78
+ # Force a reload of available geometry columns.
79
+ def geometry_columns!; end
80
+
81
+ # Force a reload of available geography columns.
82
+ def geography_columns!; end
83
+
84
+ # Grabs a geometry column based on name.
85
+ def geometry_column_by_name(name); end
86
+
87
+ # Grabs a geography column based on name.
88
+ def geography_column_by_name(name); end
89
+
90
+ # Returns both the geometry and geography columns for a table.
91
+ def spatial_columns
92
+ self.geometry_columns + self.geography_columns
93
+ end
94
+
95
+ # Reloads both the geometry and geography columns for a table.
96
+ def spatial_columns!
97
+ self.geometry_columns! + self.geography_columns!
98
+ end
99
+
100
+ # Grabs a spatial column based on name.
101
+ def spatial_column_by_name(name)
102
+ self.geometry_column_by_name(name) || self.geography_column_by_name(name)
103
+ end
104
+
105
+ %w{ geometry geography }.each do |m|
106
+ src, line = <<-EOF, __LINE__ + 1
107
+ def #{m}_columns
108
+ if @#{m}_columns.nil?
109
+ @#{m}_columns = connection.#{m}_columns(self.table_name)
110
+ @#{m}_columns.freeze
111
+ end
112
+ @#{m}_columns
113
+ end
114
+
115
+ def #{m}_columns!
116
+ @#{m}_columns = nil
117
+ #{m}_columns
118
+ end
119
+
120
+ def #{m}_column_by_name(name)
121
+ @#{m}_column_by_name ||= self.#{m}_columns.inject(HashWithIndifferentAccess.new) do |memo, obj|
122
+ memo[obj.name] = obj
123
+ memo
124
+ end
125
+ @#{m}_column_by_name[name]
126
+ end
127
+ EOF
128
+ self.class_eval(src, __FILE__, line)
129
+ end
130
+
131
+ # Quickly grab the SRID for a geometry column.
132
+ def srid_for(column_name)
133
+ column = self.spatial_column_by_name(column_name)
134
+ column.try(:srid) || Geos::ActiveRecord.UNKNOWN_SRID
135
+ end
136
+
137
+ # Quickly grab the number of dimensions for a geometry column.
138
+ def coord_dimension_for(column_name)
139
+ self.spatial_column_by_name(column_name).coord_dimension
140
+ end
141
+
142
+ protected
143
+ # Sets up nifty setters and getters for spatial columns.
144
+ # The methods created look like this:
145
+ #
146
+ # * spatial_column_name_geos
147
+ # * spatial_column_name_wkb
148
+ # * spatial_column_name_wkb_bin
149
+ # * spatial_column_name_wkt
150
+ # * spatial_column_name_ewkb
151
+ # * spatial_column_name_ewkb_bin
152
+ # * spatial_column_name_ewkt
153
+ # * spatial_column_name=(geom)
154
+ # * spatial_column_name(options = {})
155
+ #
156
+ # Where "spatial_column_name" is the name of the actual
157
+ # column.
158
+ #
159
+ # You can specify which spatial columns you want to apply
160
+ # these accessors using the :only and :except options.
161
+ def create_spatial_column_accessors!(options = nil)
162
+ options = {
163
+ :geometry_columns => true,
164
+ :geography_columns => true
165
+ }
166
+
167
+ create_these = []
168
+
169
+ if options.nil?
170
+ create_these.concat(self.spatial_columns)
171
+ else
172
+ if options[:geometry_columns]
173
+ create_these.concat(self.geometry_columns)
174
+ end
175
+
176
+ if options[:geography_columns]
177
+ create_these.concat(self.geography_columns)
178
+ end
179
+
180
+ if options[:except] && options[:only]
181
+ raise ArgumentError, "You can only specify either :except or :only (#{options.keys.inspect})"
182
+ elsif options[:except]
183
+ except = Array(options[:except]).collect(&:to_s)
184
+ create_these.reject! { |c| except.include?(c) }
185
+ elsif options[:only]
186
+ only = Array(options[:only]).collect(&:to_s)
187
+ create_these.select! { |c| only.include?(c) }
188
+ end
189
+ end
190
+
191
+ create_these.each do |k|
192
+ src, line = <<-EOF, __LINE__ + 1
193
+ def #{k.name}=(geom)
194
+ if !geom
195
+ self['#{k.name}'] = nil
196
+ else
197
+ column = self.class.spatial_column_by_name(#{k.name.inspect})
198
+
199
+ if geom =~ /^SRID=default;/i
200
+ geom = geom.sub(/default/i, column.srid.to_s)
201
+ end
202
+
203
+ geos = Geos.read(geom)
204
+
205
+ if column.spatial_type != :geography
206
+ geom_srid = if geos.srid == 0 || geos.srid == -1
207
+ Geos::ActiveRecord.UNKNOWN_SRIDS[column.spatial_type]
208
+ else
209
+ geos.srid
210
+ end
211
+
212
+ if column.srid != geom_srid
213
+ if column.srid == Geos::ActiveRecord.UNKNOWN_SRIDS[column.spatial_type] || geom_srid == Geos::ActiveRecord.UNKNOWN_SRIDS[column.spatial_type]
214
+ geos.srid = column.srid
215
+ else
216
+ raise CantConvertSRID.new(self.class.table_name, #{k.name.inspect}, geom_srid, column.srid)
217
+ end
218
+ end
219
+
220
+ self['#{k.name}'] = geos.to_ewkb
221
+ else
222
+ self['#{k.name}'] = geos.to_wkb
223
+ end
224
+ end
225
+
226
+ SPATIAL_COLUMN_OUTPUT_FORMATS.each do |f|
227
+ instance_variable_set("@#{k.name}_\#{f}", nil)
228
+ end
229
+ end
230
+
231
+ def #{k.name}_geos
232
+ @#{k.name}_geos ||= Geos.from_wkb(self['#{k.name}'])
233
+ end
234
+
235
+ def #{k.name}(options = {})
236
+ format = case options
237
+ when String, Symbol
238
+ options
239
+ when Hash
240
+ options = options.stringify_keys
241
+ options['format'] if options['format']
242
+ end
243
+
244
+ if format
245
+ if SPATIAL_COLUMN_OUTPUT_FORMATS.include?(format)
246
+ return self.send(:"#{k.name}_\#{format}")
247
+ else
248
+ raise ArgumentError, "Invalid option: \#{options[:format]}"
249
+ end
250
+ end
251
+
252
+ self['#{k.name}']
253
+ end
254
+ EOF
255
+ self.class_eval(src, __FILE__, line)
256
+
257
+ SPATIAL_COLUMN_OUTPUT_FORMATS.reject { |f| f == 'geos' }.each do |f|
258
+ src, line = <<-EOF, __LINE__ + 1
259
+ def #{k.name}_#{f}(*args)
260
+ @#{k.name}_#{f} ||= self.#{k.name}_geos.to_#{f}(*args) rescue nil
261
+ end
262
+ EOF
263
+ self.class_eval(src, __FILE__, line)
264
+ end
265
+ end
266
+ end
267
+
268
+ # Creates column accessors for geometry columns only.
269
+ def create_geometry_column_accessors!(options = {})
270
+ options = {
271
+ :geometry_columns => true
272
+ }.merge(options)
273
+
274
+ create_spatial_column_accessors!(options)
275
+ end
276
+
277
+ # Creates column accessors for geometry columns only.
278
+ def create_geography_column_accessors!(options = {})
279
+ options = {
280
+ :geography_columns => true
281
+ }.merge(options)
282
+
283
+ create_spatial_column_accessors!(options)
284
+ end
285
+
286
+ # Stubs for documentation purposes:
287
+
288
+ # Returns a Geos::Geometry object.
289
+ def __spatial_column_name_geos; end
290
+
291
+ # Returns a hex-encoded WKB String.
292
+ def __spatial_column_name_wkb; end
293
+
294
+ # Returns a WKB String in binary.
295
+ def __spatial_column_name_wkb_bin; end
296
+
297
+ # Returns a WKT String.
298
+ def __spatial_column_name_wkt; end
299
+
300
+ # Returns a hex-encoded EWKB String.
301
+ def __spatial_column_name_ewkb; end
302
+
303
+ # Returns an EWKB String in binary.
304
+ def __spatial_column_name_ewkb_bin; end
305
+
306
+ # Returns an EWKT String.
307
+ def __spatial_column_name_ewkt; end
308
+
309
+ # An enhanced setter that tries to deduce how you're
310
+ # setting the value. The setter can handle Geos::Geometry
311
+ # objects, WKT, EWKT and WKB and EWKB in both hex and
312
+ # binary.
313
+ #
314
+ # When dealing with SRIDs, you can have the SRID set
315
+ # automatically on WKT by setting the value as
316
+ # "SRID=default;GEOMETRY(...)", i.e.:
317
+ #
318
+ # spatial_column_name = "SRID=default;POINT(1.0 1.0)"
319
+ #
320
+ # The SRID will be filled in automatically if available.
321
+ # Note that we're only setting the SRID on the geometry,
322
+ # but we're not doing any sort of re-projection or anything
323
+ # of the sort. If you need to convert from one SRID to
324
+ # another, you're stuck for the moment, but we'll be adding
325
+ # support for reprojections/transoformations via proj4rb
326
+ # soon.
327
+ #
328
+ # For WKB, you're better off manipulating the WKB directly
329
+ # or using proper Geos geometry objects.
330
+ def __spatial_column_name=(geom); end
331
+
332
+ # An enhanced getter that accepts an options Hash or
333
+ # String/Symbol that can be used to determine the output
334
+ # format. In the options Hash, use :format, or set the
335
+ # format directly as a String or Symbol.
336
+ #
337
+ # This basically allows you to do the following, which
338
+ # are equivalent:
339
+ #
340
+ # spatial_column_name(:wkt)
341
+ # spatial_column_name(:format => :wkt)
342
+ # spatial_column_name_wkt
343
+ def __spatial_column_name(options = {}); end
344
+
345
+ undef __spatial_column_name_geos
346
+ undef __spatial_column_name_wkb
347
+ undef __spatial_column_name_wkb_bin
348
+ undef __spatial_column_name_wkt
349
+ undef __spatial_column_name_ewkb
350
+ undef __spatial_column_name_ewkb_bin
351
+ undef __spatial_column_name_ewkt
352
+
353
+ #undef __spatial_column_name_geojson
354
+ #undef __spatial_column_name_geohash
355
+ #undef __spatial_column_name_hexweb
356
+ #undef __spatial_column_name_kml
357
+ #undef __spatial_column_name_svg
358
+ #undef __spatial_column_name_x3d
359
+ #undef __spatial_column_name_lat_lon_text
360
+
361
+ undef __spatial_column_name=
362
+ undef __spatial_column_name
363
+ end
364
+ end
365
+
366
+ # Alias for backwards compatibility.
367
+ GeometryColumns = SpatialColumns
368
+ end
369
+ end