geos-extensions 0.0.2

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/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