geos-extensions 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2011 2167961 Ontario Inc., Zoocasa <code@zoocasa.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.rdoc ADDED
@@ -0,0 +1,99 @@
1
+
2
+ == Zoocasa GEOS Extensions
3
+
4
+ The Zoocasa GEOS Extensions library (ZGEL) is a set of utilities and tools that
5
+ extend the GEOS Ruby bindings module. From http://geos.refractions.net/ ...
6
+
7
+ GEOS (Geometry Engine - Open Source) is a C++ port of the Java Topology
8
+ Suite (JTS). As such, it aims to contain the complete functionality of JTS
9
+ in C++. This includes all the OpenGIS Simple Features for SQL spatial
10
+ predicate functions and spatial operators, as well as specific JTS
11
+ enhanced topology functions.
12
+
13
+ The GEOS bindings for Ruby can be installed by installing the GEOS library
14
+ itself using the --enable-ruby switch during configure or can often be
15
+ installed via package managers. There is also an FFI-based Ruby gem being
16
+ written available at https://github.com/dark-panda/ffi-geos .
17
+
18
+ ZGEL contains a number of enhancements to the GEOS Ruby library:
19
+
20
+ * a host of helper methods to make reading and writing to and from WKT
21
+ and WKB easier. For instance, rather than
22
+
23
+ Geos::WktReader.new.read('POINT(0 0')
24
+
25
+ you can quickly use
26
+
27
+ Geos.read('POINT(0 0)')
28
+
29
+ The Geos.read method also works with WKB in both binary and hex,
30
+ recognizes EWKB and EWKT and can read several of Google Maps
31
+ JavaScript output formats that we use for our applications. There are
32
+ also similar methods for outputting to WKT and WKB such as
33
+ Geos::Geometry#to_wkt, #to_kml, #to_georss and a number of methods to
34
+ output to Google Maps API v2-style JavaScript.
35
+
36
+ * a bunch of helper methods to quickly grab some information from
37
+ geometries like Geos::Point#lat and Geos::Point#lng.
38
+
39
+ * in all, some 70+ helper methods have been added to Geos::Geometry types.
40
+
41
+ * Geos::GeometryCollection has been made an Enumerable.
42
+
43
+ We've also included some Rails integration for PostGIS, including:
44
+
45
+ * automatic detection of geometry columns and just-in-time conversions
46
+ for input and output to and from WKB when using PostGIS. This allows
47
+ you to do stuff like this with your ActiveRecord models:
48
+
49
+ m = MyModel.find(12345)
50
+ m.the_geom # => spits out the untouched geometry value as a string in WKB
51
+ m.the_geom_geos # => spits out the geometry wrapped in a Geos::Geometry object
52
+ m.the_geom = 'POINT(0 0)' # => setters will automatically make
53
+ conversions from any of the formats that the Geos.read can recognize,
54
+ so Google Maps formats, WKT, WKB, etc. are all converted
55
+ automatically.
56
+ m.the_geom_wkt # => automatically converts to a WKT string
57
+ m.the_geom_wkb_bin # => automatically converts to WKB in binary
58
+
59
+ There's also some funky SRID handling code that will automatically
60
+ look in the geometry_columns table to make conversions for you when
61
+ necessary. Saving WKT as "SRID=default; POINT(0 0)" for instance will
62
+ automatically set the SRID when saving the ActiveRecord, or the SRID
63
+ can be specified manually.
64
+
65
+ * multiple geometry columns are supported and detected for
66
+ automatically. These column accessors are all generated dynamically at
67
+ run time.
68
+
69
+ * automatic generation of named scopes for ActiveRecord models. The
70
+ usual suspects are supported:
71
+
72
+ * st_contains
73
+ * st_containsproperly
74
+ * st_covers
75
+ * st_coveredby
76
+ * st_crosses
77
+ * st_disjoint
78
+ * st_equals
79
+ * st_intersects
80
+ * st_orderingequals
81
+ * st_overlaps
82
+ * st_touches
83
+ * st_within
84
+ * st_dwithin
85
+
86
+ These let you chain together scopes to build geospatial queries:
87
+
88
+ neighbourhood = Neighbourhood.find(12345)
89
+ my_model = MyModel.active.
90
+ recent.
91
+ st_within(neighbourhood.the_geom_geos.envelope).
92
+ st_dwithin(point, 0.1).
93
+ all(
94
+ :limit => 10
95
+ )
96
+
97
+ We wrote this code for Rails 2.3 and are currently testing on Rails
98
+ 3, but it appears that everything is working as expected and is
99
+ working with Arel. (Things are looking good so far!)
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+
2
+ # -*- ruby -*-
3
+
4
+ require 'rubygems'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/testtask'
7
+ require 'rake/rdoctask'
8
+
9
+ $:.push 'lib'
10
+
11
+ begin
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ gem.name = "geos-extensions"
15
+ gem.version = "0.0.2"
16
+ gem.summary = "Extensions for the GEOS library."
17
+ gem.description = gem.summary
18
+ gem.email = "code@zoocasa.com"
19
+ gem.homepage = "http://github.com/zoocasa/geos-extensions"
20
+ gem.authors = [ "J Smith" ]
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ desc 'Test GEOS interface'
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+ desc 'Build docs'
34
+ Rake::RDocTask.new do |t|
35
+ require 'rdoc/rdoc'
36
+ t.main = 'README.rdoc'
37
+ t.rdoc_dir = 'doc'
38
+ t.rdoc_files.include('README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb')
39
+ end
40
+
@@ -0,0 +1,55 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{geos-extensions}
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["J Smith"]
12
+ s.date = %q{2011-02-17}
13
+ s.description = %q{Extensions for the GEOS library.}
14
+ s.email = %q{code@zoocasa.com}
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ "MIT-LICENSE",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "geos-extensions.gemspec",
23
+ "lib/active_record_extensions.rb",
24
+ "lib/active_record_extensions/connection_adapters/postgresql_adapter.rb",
25
+ "lib/active_record_extensions/geometry_columns.rb",
26
+ "lib/active_record_extensions/geospatial_scopes.rb",
27
+ "lib/geos-extensions.rb",
28
+ "lib/geos_extensions.rb",
29
+ "lib/geos_helper.rb",
30
+ "lib/google_maps.rb",
31
+ "lib/google_maps/polyline_encoder.rb",
32
+ "test/reader_test.rb",
33
+ "test/test_helper.rb",
34
+ "test/writer_test.rb"
35
+ ]
36
+ s.homepage = %q{http://github.com/zoocasa/geos-extensions}
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.5.2}
39
+ s.summary = %q{Extensions for the GEOS library.}
40
+ s.test_files = [
41
+ "test/reader_test.rb",
42
+ "test/test_helper.rb",
43
+ "test/writer_test.rb"
44
+ ]
45
+
46
+ if s.respond_to? :specification_version then
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
+ else
51
+ end
52
+ else
53
+ end
54
+ end
55
+
@@ -0,0 +1,8 @@
1
+
2
+ module Geos
3
+ module ActiveRecord
4
+ autoload :GeometryColumns, File.join(GEOS_EXTENSIONS_BASE, %w{ active_record_extensions geometry_columns })
5
+ autoload :GeospatialScopes, File.join(GEOS_EXTENSIONS_BASE, %w{ active_record_extensions geospatial_scopes })
6
+ end
7
+ end
8
+
@@ -0,0 +1,39 @@
1
+
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ # Allows access to the name, srid and coord_dimensions of a PostGIS
5
+ # geometry column in PostgreSQL.
6
+ class PostgreSQLGeometryColumn
7
+ attr_accessor :name, :srid, :coord_dimension
8
+
9
+ def initialize(name, srid = nil, coord_dimension = nil)
10
+ @name, @srid, @coord_dimension = name, srid, coord_dimension
11
+ end
12
+ end
13
+
14
+ class PostgreSQLAdapter < AbstractAdapter
15
+ # Returns the geometry columns for the table.
16
+ def geometry_columns(table_name, name = nil)
17
+ columns(table_name, name).select { |c| c.sql_type == 'geometry' }.collect do |c|
18
+ res = execute(
19
+ "SELECT * FROM geometry_columns WHERE f_table_name = #{quote(table_name)} AND f_geometry_column = #{quote(c.name)}",
20
+ "Geometry column load for #{table_name}"
21
+ )
22
+
23
+ returning(PostgreSQLGeometryColumn.new(c.name)) do |g|
24
+ # since we're too stupid at the moment to understand
25
+ # PostgreSQL schemas, let's just go with this:
26
+ if res.ntuples == 1
27
+ coord_dimension_idx, srid_idx =
28
+ res.fields.index('coord_dimension'),
29
+ res.fields.index('srid')
30
+
31
+ g.srid = res.getvalue(0, srid_idx).to_i
32
+ g.coord_dimension = res.getvalue(0, coord_dimension_idx).to_i
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,252 @@
1
+
2
+ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
3
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ active_record_extensions connection_adapters postgresql_adapter })
4
+ end
5
+
6
+ module Geos
7
+ module ActiveRecord #:nodoc:
8
+
9
+ # This little module helps us out with geometry columns. At least, in
10
+ # PostgreSQL it does.
11
+ #
12
+ # This module will add a method called geometry_columns to your model
13
+ # which will contain information that can be gleaned from the
14
+ # geometry_columns table that PostGIS creates.
15
+ #
16
+ # You can also have the module automagically create some accessor
17
+ # methods for you to make your life easier. These accessor methods will
18
+ # override the ActiveRecord defaults and allow you to set geometry
19
+ # column values using Geos geometry objects directly or with
20
+ # PostGIS-style extended WKT and such. See
21
+ # create_geometry_column_accessors! for details.
22
+ #
23
+ # === Caveats:
24
+ #
25
+ # * This module currently only works with PostGIS.
26
+ # * This module doesn't really "get" PostgreSQL catalogs and schemas
27
+ # and such. That would be a little more involved but it would be
28
+ # nice if Rails was aware of such things.
29
+ module GeometryColumns
30
+ GEOMETRY_COLUMN_OUTPUT_FORMATS = %{ geos wkt wkb ewkt ewkb wkb_bin ewkb_bin }.freeze
31
+
32
+ class InvalidGeometry < ::ActiveRecord::ActiveRecordError
33
+ def initialize(geom)
34
+ super("Invalid geometry: #{geom}")
35
+ end
36
+ end
37
+
38
+ class SRIDNotFound < ::ActiveRecord::ActiveRecordError
39
+ def initialize(table_name, column)
40
+ super("Couldn't find SRID for #{table_name}.#{column}")
41
+ end
42
+ end
43
+
44
+ def self.included(base) #:nodoc:
45
+ base.extend(ClassMethods)
46
+ base.send(:include, Geos::ActiveRecord::GeospatialScopes)
47
+ end
48
+
49
+ module ClassMethods
50
+ protected
51
+ @geometry_columns = nil
52
+
53
+ public
54
+ # Returns an Array of available geometry columns in the
55
+ # table. These are PostgreSQLColumns with values set for
56
+ # the srid and coord_dimensions properties.
57
+ def geometry_columns
58
+ if @geometry_columns.nil?
59
+ @geometry_columns = connection.geometry_columns(self.table_name)
60
+ @geometry_columns.freeze
61
+ end
62
+ @geometry_columns
63
+ end
64
+
65
+ # Grabs a geometry column based on name.
66
+ def geometry_column_by_name(name)
67
+ @geometry_column_by_name ||= self.geometry_columns.inject(HashWithIndifferentAccess.new) do |memo, obj|
68
+ memo[obj.name] = obj
69
+ memo
70
+ end
71
+ @geometry_column_by_name[name]
72
+ end
73
+
74
+ # Quickly grab the SRID for a geometry column.
75
+ def srid_for(column)
76
+ self.geometry_column_by_name(column).try(:srid) || -1
77
+ end
78
+
79
+ # Quickly grab the number of dimensions for a geometry column.
80
+ def coord_dimension_for(column)
81
+ self.geometry_column_by_name(column).coord_dimension
82
+ end
83
+
84
+ protected
85
+ # Sets up nifty setters and getters for geometry columns.
86
+ # The methods created look like this:
87
+ #
88
+ # * geometry_column_name_geos
89
+ # * geometry_column_name_wkb
90
+ # * geometry_column_name_wkb_bin
91
+ # * geometry_column_name_wkt
92
+ # * geometry_column_name_ewkb
93
+ # * geometry_column_name_ewkb_bin
94
+ # * geometry_column_name_ewkt
95
+ # * geometry_column_name=(geom)
96
+ # * geometry_column_name(options = {})
97
+ #
98
+ # Where "geometry_column_name" is the name of the actual
99
+ # column.
100
+ #
101
+ # You can specify which geometry columns you want to apply
102
+ # these accessors using the :only and :except options.
103
+ def create_geometry_column_accessors!(options = nil)
104
+ create_these = if options.nil?
105
+ self.geometry_columns
106
+ elsif options[:except] && options[:only]
107
+ raise ArgumentError, "You can only specify either :except or :only (#{options.keys.inspect})"
108
+ elsif options[:except]
109
+ except = Array(options[:except]).collect(&:to_s)
110
+ self.geometry_columns.reject { |c| except.include?(c) }
111
+ elsif options[:only]
112
+ only = Array(options[:only]).collect(&:to_s)
113
+ self.geometry_columns.select { |c| only.include?(c) }
114
+ end
115
+
116
+ create_these.each do |k|
117
+ src, line = <<-EOF, __LINE__ + 1
118
+ def #{k.name}=(geom)
119
+ geos = case geom
120
+ when /^SRID=default;/
121
+ if #{k.srid.inspect}
122
+ geom = geom.sub(/default/, #{k.srid.inspect}.to_s)
123
+ Geos.from_wkt(geom)
124
+ else
125
+ raise SRIDNotFound.new(self.table_name, #{k.name})
126
+ end
127
+ else
128
+ Geos.read(geom)
129
+ end
130
+
131
+ self['#{k.name}'] = if geos
132
+ if geos.srid == 0
133
+ geos.to_wkb
134
+ else
135
+ geos.to_ewkb
136
+ end
137
+ end
138
+
139
+ GEOMETRY_COLUMN_OUTPUT_FORMATS.each do |f|
140
+ instance_variable_set("@#{k.name}_\#{f}", nil)
141
+ end
142
+ end
143
+
144
+ def #{k.name}_geos
145
+ @#{k.name}_geos ||= Geos.from_wkb(self['#{k.name}'])
146
+ end
147
+
148
+ def #{k.name}(options = {})
149
+ format = case options
150
+ when String, Symbol
151
+ options
152
+ when Hash
153
+ options = options.stringify_keys
154
+ options['format'] if options['format']
155
+ end
156
+
157
+ if format
158
+ if GEOMETRY_COLUMN_OUTPUT_FORMATS.include?(format)
159
+ return self.send(:"#{k.name}_\#{format}")
160
+ else
161
+ raise ArgumentError, "Invalid option: \#{options[:format]}"
162
+ end
163
+ end
164
+
165
+ self['#{k.name}']
166
+ end
167
+ EOF
168
+ self.class_eval(src, __FILE__, line)
169
+
170
+ GEOMETRY_COLUMN_OUTPUT_FORMATS.reject { |f| f == :geos }.each do |f|
171
+ src, line = <<-EOF, __LINE__ + 1
172
+ def #{k.name}_#{f}
173
+ @#{k.name}_#{f} ||= self.#{k.name}_geos.to_#{f} rescue nil
174
+ end
175
+ EOF
176
+ self.class_eval(src, __FILE__, line)
177
+ end
178
+ end
179
+ end
180
+
181
+ # Stubs for documentation purposes:
182
+
183
+ # Returns a Geos geometry.
184
+ def __geometry_column_name_geos; end
185
+
186
+ # Returns a hex-encoded WKB String.
187
+ def __geometry_column_name_wkb; end
188
+
189
+ # Returns a WKB String in binary.
190
+ def __geometry_column_name_wkb_bin; end
191
+
192
+ # Returns a WKT String.
193
+ def __geometry_column_name_wkt; end
194
+
195
+ # Returns a hex-encoded EWKB String.
196
+ def __geometry_column_name_ewkb; end
197
+
198
+ # Returns an EWKB String in binary.
199
+ def __geometry_column_name_ewkb_bin; end
200
+
201
+ # Returns an EWKT String.
202
+ def __geometry_column_name_ewkt; end
203
+
204
+ # An enhanced setter that tries to deduce how you're
205
+ # setting the value. The setter can handle Geos::Geometry
206
+ # objects, WKT, EWKT and WKB and EWKB in both hex and
207
+ # binary.
208
+ #
209
+ # When dealing with SRIDs, you can have the SRID set
210
+ # automatically on WKT by setting the value as
211
+ # "SRID=default;GEOMETRY(...)", i.e.:
212
+ #
213
+ # geometry_column_name = "SRID=default;POINT(1.0 1.0)"
214
+ #
215
+ # The SRID will be filled in automatically if available.
216
+ # Note that we're only setting the SRID on the geometry,
217
+ # but we're not doing any sort of re-projection or anything
218
+ # of the sort. If you need to convert from one SRID to
219
+ # another, you're stuck for the moment, but we'll be adding
220
+ # support for reprojections/transoformations via proj4rb
221
+ # soon.
222
+ #
223
+ # For WKB, you're better off manipulating the WKB directly
224
+ # or using proper Geos geometry objects.
225
+ def __geometry_column_name=(geom); end
226
+
227
+ # An enhanced getter that accepts an options Hash or
228
+ # String/Symbol that can be used to determine the output
229
+ # format. In the options Hash, use :format, or set the
230
+ # format directly as a String or Symbol.
231
+ #
232
+ # This basically allows you to do the following, which
233
+ # are equivalent:
234
+ #
235
+ # geometry_column_name(:wkt)
236
+ # geometry_column_name(:format => :wkt)
237
+ # geometry_column_name_wkt
238
+ def __geometry_column_name(options = {}); end
239
+
240
+ undef __geometry_column_name_geos
241
+ undef __geometry_column_name_wkb
242
+ undef __geometry_column_name_wkb_bin
243
+ undef __geometry_column_name_wkt
244
+ undef __geometry_column_name_ewkb
245
+ undef __geometry_column_name_ewkb_bin
246
+ undef __geometry_column_name_ewkt
247
+ undef __geometry_column_name=
248
+ undef __geometry_column_name
249
+ end
250
+ end
251
+ end
252
+ end