geos-extensions 0.1.6 → 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/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