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 +23 -0
- data/README.rdoc +99 -0
- data/Rakefile +40 -0
- data/geos-extensions.gemspec +55 -0
- data/lib/active_record_extensions.rb +8 -0
- data/lib/active_record_extensions/connection_adapters/postgresql_adapter.rb +39 -0
- data/lib/active_record_extensions/geometry_columns.rb +252 -0
- data/lib/active_record_extensions/geospatial_scopes.rb +171 -0
- data/lib/geos-extensions.rb +9 -0
- data/lib/geos_extensions.rb +849 -0
- data/lib/geos_helper.rb +51 -0
- data/lib/google_maps.rb +7 -0
- data/lib/google_maps/polyline_encoder.rb +60 -0
- data/test/reader_test.rb +112 -0
- data/test/test_helper.rb +71 -0
- data/test/writer_test.rb +287 -0
- metadata +84 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
|
2
|
+
module Geos
|
3
|
+
module ActiveRecord
|
4
|
+
|
5
|
+
# Creates named scopes for geospatial relationships. The scopes created
|
6
|
+
# follow the nine relationships established by the standard
|
7
|
+
# Dimensionally Extended 9-Intersection Matrix functions plus a couple
|
8
|
+
# of extra ones provided by PostGIS.
|
9
|
+
#
|
10
|
+
# Scopes provided are:
|
11
|
+
#
|
12
|
+
# * st_contains
|
13
|
+
# * st_containsproperly
|
14
|
+
# * st_covers
|
15
|
+
# * st_coveredby
|
16
|
+
# * st_crosses
|
17
|
+
# * st_disjoint
|
18
|
+
# * st_equals
|
19
|
+
# * st_intersects
|
20
|
+
# * st_orderingequals
|
21
|
+
# * st_overlaps
|
22
|
+
# * st_touches
|
23
|
+
# * st_within
|
24
|
+
# * st_dwithin
|
25
|
+
#
|
26
|
+
# The first argument to each method is can be a Geos::Geometry-based
|
27
|
+
# object or anything readable by Geos.read along with an optional
|
28
|
+
# options Hash.
|
29
|
+
#
|
30
|
+
# == Options
|
31
|
+
#
|
32
|
+
# * :column - the column to compare against. The default is 'the_geom'.
|
33
|
+
# * :use_index - whether to use the "ST_" methods or the "\_ST_"
|
34
|
+
# variants which don't use indexes. The default is true.
|
35
|
+
# * :wkb_options - in order to facilitate some conversions, geometries
|
36
|
+
# are converted to WKB. The default is `{:include_srid => true}` to
|
37
|
+
# force the geometry to use PostGIS's Extended WKB.
|
38
|
+
#
|
39
|
+
# == SRID Detection
|
40
|
+
#
|
41
|
+
# * if the geometry itself has an SRID, we'll compare it to the
|
42
|
+
# geometry of the column. If they differ, we'll use ST_Transform
|
43
|
+
# to transform the geometry to the proper SRID for comparison. If
|
44
|
+
# they're the same, no conversion is necessary.
|
45
|
+
# * if no SRID is specified in the geometry, we'll use ST_SetSRID
|
46
|
+
# to set the SRID to the column's SRID.
|
47
|
+
# * in cases where the column has been defined with an SRID of -1
|
48
|
+
# (PostGIS's default), no transformation is done, but we'll set the
|
49
|
+
# SRID of the geometry to -1 to perform the query using ST_SetSRID,
|
50
|
+
# as we'll assume the SRID of the column to be whatever the SRID of
|
51
|
+
# the geometry is.
|
52
|
+
module GeospatialScopes
|
53
|
+
SCOPE_METHOD = if Rails.version >= '3.0'
|
54
|
+
'scope'
|
55
|
+
else
|
56
|
+
'named_scope'
|
57
|
+
end
|
58
|
+
|
59
|
+
RELATIONSHIPS = %w{
|
60
|
+
contains
|
61
|
+
containsproperly
|
62
|
+
covers
|
63
|
+
coveredby
|
64
|
+
crosses
|
65
|
+
disjoint
|
66
|
+
equals
|
67
|
+
intersects
|
68
|
+
orderingequals
|
69
|
+
overlaps
|
70
|
+
touches
|
71
|
+
within
|
72
|
+
}.freeze
|
73
|
+
|
74
|
+
def self.included(base)
|
75
|
+
RELATIONSHIPS.each do |relationship|
|
76
|
+
src, line = <<-EOF, __LINE__ + 1
|
77
|
+
#{SCOPE_METHOD} :st_#{relationship}, lambda { |*args|
|
78
|
+
raise ArgumentError.new("wrong number of arguments (\#{args.length} for 1-2)") unless
|
79
|
+
args.length.between?(1, 2)
|
80
|
+
|
81
|
+
options = {
|
82
|
+
:column => 'the_geom',
|
83
|
+
:use_index => true
|
84
|
+
}.merge(args.extract_options!)
|
85
|
+
|
86
|
+
geom = Geos.read(args.first)
|
87
|
+
column_name = ::ActiveRecord::Base.connection.quote_table_name(options[:column])
|
88
|
+
column_srid = self.srid_for(options[:column])
|
89
|
+
geom_srid = if geom.srid == 0
|
90
|
+
-1
|
91
|
+
else
|
92
|
+
geom.srid
|
93
|
+
end
|
94
|
+
|
95
|
+
function = if options[:use_index]
|
96
|
+
"ST_#{relationship}"
|
97
|
+
else
|
98
|
+
"_ST_#{relationship}"
|
99
|
+
end
|
100
|
+
|
101
|
+
conditions = if column_srid != geom_srid
|
102
|
+
if column_srid == -1 || geom_srid == -1
|
103
|
+
%{\#{function}(\#{column_name}, ST_SetSRID(?, \#{column_srid}))}
|
104
|
+
else
|
105
|
+
%{\#{function}(\#{column_name}, ST_Transform(?, \#{column_srid}))}
|
106
|
+
end
|
107
|
+
else
|
108
|
+
%{\#{function}(\#{column_name}, ?)}
|
109
|
+
end
|
110
|
+
|
111
|
+
{
|
112
|
+
:conditions => [
|
113
|
+
conditions,
|
114
|
+
geom.to_ewkb
|
115
|
+
]
|
116
|
+
}
|
117
|
+
}
|
118
|
+
EOF
|
119
|
+
base.class_eval(src, __FILE__, line)
|
120
|
+
end
|
121
|
+
|
122
|
+
src, line = <<-EOF, __LINE__ + 1
|
123
|
+
#{SCOPE_METHOD} :st_dwithin, lambda { |*args|
|
124
|
+
raise ArgumentError.new("wrong number of arguments (\#{args.length} for 2-3)") unless
|
125
|
+
args.length.between?(2, 3)
|
126
|
+
|
127
|
+
options = {
|
128
|
+
:column => 'the_geom',
|
129
|
+
:use_index => true
|
130
|
+
}.merge(args.extract_options!)
|
131
|
+
|
132
|
+
geom, distance = Geos.read(args.first), args[1]
|
133
|
+
|
134
|
+
column_name = ::ActiveRecord::Base.connection.quote_table_name(options[:column])
|
135
|
+
column_srid = self.srid_for(options[:column])
|
136
|
+
geom_srid = if geom.srid == 0
|
137
|
+
-1
|
138
|
+
else
|
139
|
+
geom.srid
|
140
|
+
end
|
141
|
+
|
142
|
+
function = if options[:use_index]
|
143
|
+
'ST_dwithin'
|
144
|
+
else
|
145
|
+
'_ST_dwithin'
|
146
|
+
end
|
147
|
+
|
148
|
+
conditions = if column_srid != geom_srid
|
149
|
+
if column_srid == -1 || geom_srid == -1
|
150
|
+
%{\#{function}(\#{column_name}, ST_SetSRID(?, \#{column_srid}), ?)}
|
151
|
+
else
|
152
|
+
%{\#{function}(\#{column_name}, ST_Transform(?, \#{column_srid}), ?)}
|
153
|
+
end
|
154
|
+
else
|
155
|
+
%{\#{function}(\#{column_name}, ?, ?)}
|
156
|
+
end
|
157
|
+
|
158
|
+
{
|
159
|
+
:conditions => [
|
160
|
+
conditions,
|
161
|
+
geom.to_ewkb,
|
162
|
+
distance
|
163
|
+
]
|
164
|
+
}
|
165
|
+
}
|
166
|
+
EOF
|
167
|
+
base.class_eval(src, __FILE__, line)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), *%w{ geos_extensions })
|
3
|
+
|
4
|
+
if defined?(Rails)
|
5
|
+
require File.join(GEOS_EXTENSIONS_BASE, *%w{ active_record_extensions connection_adapters postgresql_adapter })
|
6
|
+
require File.join(GEOS_EXTENSIONS_BASE, *%w{ active_record_extensions geometry_columns })
|
7
|
+
require File.join(GEOS_EXTENSIONS_BASE, *%w{ active_record_extensions geospatial_scopes })
|
8
|
+
end
|
9
|
+
|
@@ -0,0 +1,849 @@
|
|
1
|
+
|
2
|
+
GEOS_EXTENSIONS_BASE = File.join(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
begin
|
5
|
+
if !ENV['USE_BINARY_GEOS']
|
6
|
+
require 'ffi-geos'
|
7
|
+
end
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'geos' unless defined?(Geos)
|
12
|
+
|
13
|
+
# Some custom extensions to the SWIG-based Geos Ruby extension.
|
14
|
+
module Geos
|
15
|
+
autoload :Helper, File.join(GEOS_EXTENSIONS_BASE, 'geos_helper')
|
16
|
+
autoload :ActiveRecord, File.join(GEOS_EXTENSIONS_BASE, 'active_record_extensions')
|
17
|
+
autoload :GoogleMaps, File.join(GEOS_EXTENSIONS_BASE, 'google_maps')
|
18
|
+
|
19
|
+
REGEXP_WKT = /^(?:SRID=([0-9]+);)?(\s*[PLMCG].+)/i
|
20
|
+
REGEXP_WKB_HEX = /^[A-Fa-f0-9\s]+$/
|
21
|
+
REGEXP_G_LAT_LNG_BOUNDS = /^
|
22
|
+
\(
|
23
|
+
\(
|
24
|
+
(-?\d+(?:\.\d+)?) # sw lat or x
|
25
|
+
\s*,\s*
|
26
|
+
(-?\d+(?:\.\d+)?) # sw lng or y
|
27
|
+
\)
|
28
|
+
\s*,\s*
|
29
|
+
\(
|
30
|
+
(-?\d+(?:\.\d+)?) # ne lat or x
|
31
|
+
\s*,\s*
|
32
|
+
(-?\d+(?:\.\d+)?) # ne lng or y
|
33
|
+
\)
|
34
|
+
\)
|
35
|
+
$/x
|
36
|
+
REGEXP_G_LAT_LNG = /^
|
37
|
+
\(?
|
38
|
+
(-?\d+(?:\.\d+)?) # lat or x
|
39
|
+
\s*,\s*
|
40
|
+
(-?\d+(?:\.\d+)?) # lng or y
|
41
|
+
\)?
|
42
|
+
$/x
|
43
|
+
|
44
|
+
def self.wkb_reader_singleton
|
45
|
+
Thread.current[:geos_extensions_wkb_reader] ||= WkbReader.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.wkt_reader_singleton
|
49
|
+
Thread.current[:geos_extensions_wkt_reader] ||= WktReader.new
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns some kind of Geometry object from the given WKB in
|
53
|
+
# binary.
|
54
|
+
def self.from_wkb_bin(wkb)
|
55
|
+
self.wkb_reader_singleton.read(wkb)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns some kind of Geometry object from the given WKB in hex.
|
59
|
+
def self.from_wkb(wkb)
|
60
|
+
self.wkb_reader_singleton.read_hex(wkb)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Tries its best to return a Geometry object.
|
64
|
+
def self.read(geom, options = {})
|
65
|
+
geos = case geom
|
66
|
+
when Geos::Geometry
|
67
|
+
geom
|
68
|
+
when REGEXP_WKT
|
69
|
+
Geos.from_wkt(geom)
|
70
|
+
when REGEXP_WKB_HEX
|
71
|
+
Geos.from_wkb(geom)
|
72
|
+
when REGEXP_G_LAT_LNG_BOUNDS, REGEXP_G_LAT_LNG
|
73
|
+
Geos.from_g_lat_lng(geom, options)
|
74
|
+
when String
|
75
|
+
Geos.from_wkb(geom.unpack('H*').first.upcase)
|
76
|
+
when nil
|
77
|
+
nil
|
78
|
+
else
|
79
|
+
raise ArgumentError.new("Invalid geometry!")
|
80
|
+
end
|
81
|
+
|
82
|
+
if geos && options[:srid]
|
83
|
+
geos.srid = options[:srid]
|
84
|
+
end
|
85
|
+
|
86
|
+
geos
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns some kind of Geometry object from the given WKT. This method
|
90
|
+
# will also accept PostGIS-style EWKT and its various enhancements.
|
91
|
+
def self.from_wkt(wkt)
|
92
|
+
srid, raw_wkt = wkt.scan(REGEXP_WKT).first
|
93
|
+
geom = self.wkt_reader_singleton.read(raw_wkt.upcase)
|
94
|
+
geom.srid = srid.to_i if srid
|
95
|
+
geom
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns some kind of Geometry object from a String provided by a Google
|
99
|
+
# Maps object. For instance, calling toString() on a GLatLng will output
|
100
|
+
# (lat, lng), while calling on a GLatLngBounds will produce
|
101
|
+
# ((sw lat, sw lng), (ne lat, ne lng)). This method handles both GLatLngs
|
102
|
+
# and GLatLngBounds. In the case of GLatLngs, we return a new Geos::Point,
|
103
|
+
# while for GLatLngBounds we return a Geos::Polygon that encompasses the
|
104
|
+
# bounds. Use the option :points to interpret the incoming value as
|
105
|
+
# as GPoints rather than GLatLngs.
|
106
|
+
def self.from_g_lat_lng(geometry, options = {})
|
107
|
+
geom = case geometry
|
108
|
+
when REGEXP_G_LAT_LNG_BOUNDS
|
109
|
+
coords = Array.new
|
110
|
+
$~.captures.each_slice(2) { |f|
|
111
|
+
coords << f.collect(&:to_f)
|
112
|
+
}
|
113
|
+
|
114
|
+
unless options[:points]
|
115
|
+
coords.each do |c|
|
116
|
+
c.reverse!
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
Geos.from_wkt("LINESTRING(%s, %s)" % [
|
121
|
+
coords[0].join(' '),
|
122
|
+
coords[1].join(' ')
|
123
|
+
]).envelope
|
124
|
+
when REGEXP_G_LAT_LNG
|
125
|
+
coords = $~.captures.collect(&:to_f).tap { |c|
|
126
|
+
c.reverse! unless options[:points]
|
127
|
+
}
|
128
|
+
Geos.from_wkt("POINT(#{coords.join(' ')})")
|
129
|
+
else
|
130
|
+
raise "Invalid GLatLng format"
|
131
|
+
end
|
132
|
+
|
133
|
+
if options[:srid]
|
134
|
+
geom.srid = options[:srid]
|
135
|
+
end
|
136
|
+
|
137
|
+
geom
|
138
|
+
end
|
139
|
+
|
140
|
+
# Same as from_g_lat_lng but uses GPoints instead of GLatLngs and GBounds
|
141
|
+
# instead of GLatLngBounds. Equivalent to calling from_g_lat_lng with a
|
142
|
+
# non-false expression for the points parameter.
|
143
|
+
def self.from_g_point(geometry, options = {})
|
144
|
+
self.from_g_lat_lng(geometry, options.merge(:points => true))
|
145
|
+
end
|
146
|
+
|
147
|
+
# This is our base module that we use for some generic methods used all
|
148
|
+
# over the place.
|
149
|
+
class Geometry
|
150
|
+
protected
|
151
|
+
|
152
|
+
WKB_WRITER_OPTIONS = [ :output_dimensions, :byte_order, :include_srid ].freeze
|
153
|
+
def wkb_writer(options = {}) #:nodoc:
|
154
|
+
writer = WkbWriter.new
|
155
|
+
options.reject { |k, v| !WKB_WRITER_OPTIONS.include?(k) }.each do |k, v|
|
156
|
+
writer.send("#{k}=", v)
|
157
|
+
end
|
158
|
+
writer
|
159
|
+
end
|
160
|
+
|
161
|
+
public
|
162
|
+
|
163
|
+
# Spits the geometry out into WKB in binary.
|
164
|
+
#
|
165
|
+
# You can set the :output_dimensions, :byte_order and :include_srid
|
166
|
+
# options via the options Hash.
|
167
|
+
def to_wkb_bin(options = {})
|
168
|
+
wkb_writer(options).write(self)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Quickly call to_wkb_bin with :include_srid set to true.
|
172
|
+
def to_ewkb_bin(options = {})
|
173
|
+
options = {
|
174
|
+
:include_srid => true
|
175
|
+
}.merge options
|
176
|
+
to_wkb_bin(options)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Spits the geometry out into WKB in hex.
|
180
|
+
#
|
181
|
+
# You can set the :output_dimensions, :byte_order and :include_srid
|
182
|
+
# options via the options Hash.
|
183
|
+
def to_wkb(options = {})
|
184
|
+
wkb_writer(options).write_hex(self)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Quickly call to_wkb with :include_srid set to true.
|
188
|
+
def to_ewkb(options = {})
|
189
|
+
options = {
|
190
|
+
:include_srid => true
|
191
|
+
}.merge options
|
192
|
+
to_wkb(options)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Spits the geometry out into WKT. You can specify the :include_srid
|
196
|
+
# option to create a PostGIS-style EWKT output.
|
197
|
+
def to_wkt(options = {})
|
198
|
+
writer = WktWriter.new
|
199
|
+
ret = ''
|
200
|
+
ret << "SRID=#{self.srid};" if options[:include_srid]
|
201
|
+
ret << writer.write(self)
|
202
|
+
ret
|
203
|
+
end
|
204
|
+
|
205
|
+
# Quickly call to_wkt with :include_srid set to true.
|
206
|
+
def to_ewkt(options = {})
|
207
|
+
options = {
|
208
|
+
:include_srid => true
|
209
|
+
}.merge options
|
210
|
+
to_wkt(options)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns a Point for the envelope's upper left coordinate.
|
214
|
+
def upper_left
|
215
|
+
if @upper_left
|
216
|
+
@upper_left
|
217
|
+
else
|
218
|
+
cs = self.envelope.exterior_ring.coord_seq
|
219
|
+
@upper_left = Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(3)} #{cs.get_y(3)})")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
alias :nw :upper_left
|
223
|
+
alias :northwest :upper_left
|
224
|
+
|
225
|
+
# Returns a Point for the envelope's upper right coordinate.
|
226
|
+
def upper_right
|
227
|
+
if @upper_right
|
228
|
+
@upper_right
|
229
|
+
else
|
230
|
+
cs = self.envelope.exterior_ring.coord_seq
|
231
|
+
@upper_right ||= Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(2)} #{cs.get_y(2)})")
|
232
|
+
end
|
233
|
+
end
|
234
|
+
alias :ne :upper_right
|
235
|
+
alias :northeast :upper_right
|
236
|
+
|
237
|
+
# Returns a Point for the envelope's lower right coordinate.
|
238
|
+
def lower_right
|
239
|
+
if @lower_right
|
240
|
+
@lower_right
|
241
|
+
else
|
242
|
+
cs = self.envelope.exterior_ring.coord_seq
|
243
|
+
@lower_right ||= Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(1)} #{cs.get_y(1)})")
|
244
|
+
end
|
245
|
+
end
|
246
|
+
alias :se :lower_right
|
247
|
+
alias :southeast :lower_right
|
248
|
+
|
249
|
+
# Returns a Point for the envelope's lower left coordinate.
|
250
|
+
def lower_left
|
251
|
+
if @lower_left
|
252
|
+
@lower_left
|
253
|
+
else
|
254
|
+
cs = self.envelope.exterior_ring.coord_seq
|
255
|
+
@lower_left ||= Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(0)} #{cs.get_y(0)})")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
alias :sw :lower_left
|
259
|
+
alias :southwest :lower_left
|
260
|
+
|
261
|
+
# Northern-most Y coordinate.
|
262
|
+
def top
|
263
|
+
@top ||= self.upper_right.to_a[1]
|
264
|
+
end
|
265
|
+
alias :n :top
|
266
|
+
alias :north :top
|
267
|
+
|
268
|
+
# Eastern-most X coordinate.
|
269
|
+
def right
|
270
|
+
@right ||= self.upper_right.to_a[0]
|
271
|
+
end
|
272
|
+
alias :e :right
|
273
|
+
alias :east :right
|
274
|
+
|
275
|
+
# Southern-most Y coordinate.
|
276
|
+
def bottom
|
277
|
+
@bottom ||= self.lower_left.to_a[1]
|
278
|
+
end
|
279
|
+
alias :s :bottom
|
280
|
+
alias :south :bottom
|
281
|
+
|
282
|
+
# Western-most X coordinate.
|
283
|
+
def left
|
284
|
+
@left ||= self.lower_left.to_a[0]
|
285
|
+
end
|
286
|
+
alias :w :left
|
287
|
+
alias :west :left
|
288
|
+
|
289
|
+
# Returns a new GLatLngBounds object with the proper GLatLngs in place
|
290
|
+
# for determining the geometry bounds.
|
291
|
+
def to_g_lat_lng_bounds(options = {})
|
292
|
+
klass = if options[:short_class]
|
293
|
+
'GLatLngBounds'
|
294
|
+
else
|
295
|
+
'google.maps.LatLngBounds'
|
296
|
+
end
|
297
|
+
|
298
|
+
"new #{klass}(#{self.lower_left.to_g_lat_lng(options)}, #{self.upper_right.to_g_lat_lng(options)})"
|
299
|
+
end
|
300
|
+
|
301
|
+
# Returns a String in Google Maps' GLatLngBounds#toString() format.
|
302
|
+
def to_g_lat_lng_bounds_string(precision = 10)
|
303
|
+
"((#{self.lower_left.to_g_url_value(precision)}), (#{self.upper_right.to_g_url_value(precision)}))"
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns a new GPolyline.
|
307
|
+
def to_g_polyline polyline_options = {}, options = {}
|
308
|
+
self.coord_seq.to_g_polyline polyline_options, options
|
309
|
+
end
|
310
|
+
|
311
|
+
# Returns a new GPolygon.
|
312
|
+
def to_g_polygon polygon_options = {}, options = {}
|
313
|
+
self.coord_seq.to_g_polygon polygon_options, options
|
314
|
+
end
|
315
|
+
|
316
|
+
# Returns a new GMarker at the centroid of the geometry. The options
|
317
|
+
# Hash works the same as the Google Maps API GMarkerOptions class does,
|
318
|
+
# but allows for underscored Ruby-like options which are then converted
|
319
|
+
# to the appropriate camel-cased Javascript options.
|
320
|
+
def to_g_marker marker_options = {}, options = {}
|
321
|
+
klass = if options[:short_class]
|
322
|
+
'GMarker'
|
323
|
+
else
|
324
|
+
'google.maps.Marker'
|
325
|
+
end
|
326
|
+
|
327
|
+
opts = marker_options.inject({}) do |memo, (k, v)|
|
328
|
+
memo[Geos::Helper.camelize(k.to_s)] = v
|
329
|
+
memo
|
330
|
+
end
|
331
|
+
|
332
|
+
"new #{klass}(#{self.centroid.to_g_lat_lng(options)}, #{opts.to_json})"
|
333
|
+
end
|
334
|
+
|
335
|
+
# Spit out Google's JSON geocoder Point format. The extra 0 is added
|
336
|
+
# on as Google's format seems to like including the Z coordinate.
|
337
|
+
def to_g_json_point
|
338
|
+
{
|
339
|
+
:coordinates => (self.centroid.to_a << 0)
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
# Spit out Google's JSON geocoder ExtendedData LatLonBox format.
|
344
|
+
def to_g_lat_lon_box
|
345
|
+
{
|
346
|
+
:north => self.north,
|
347
|
+
:east => self.east,
|
348
|
+
:south => self.south,
|
349
|
+
:west => self.west
|
350
|
+
}
|
351
|
+
end
|
352
|
+
|
353
|
+
# Spit out Google's toUrlValue format.
|
354
|
+
def to_g_url_value(precision = 6)
|
355
|
+
c = self.centroid
|
356
|
+
"#{Geos::Helper.number_with_precision(c.lat, precision)},#{Geos::Helper.number_with_precision(c.lng, precision)}"
|
357
|
+
end
|
358
|
+
|
359
|
+
# Spits out a bounding box the way Flickr likes it. You can set the
|
360
|
+
# precision of the rounding using the :precision option. In order to
|
361
|
+
# ensure that the box is indeed a box and not merely a point, the
|
362
|
+
# southwest coordinates are floored and the northeast point ceiled.
|
363
|
+
def to_flickr_bbox(options = {})
|
364
|
+
options = {
|
365
|
+
:precision => 1
|
366
|
+
}.merge(options)
|
367
|
+
precision = 10.0 ** options[:precision]
|
368
|
+
|
369
|
+
[
|
370
|
+
(self.west * precision).floor / precision,
|
371
|
+
(self.south * precision).floor / precision,
|
372
|
+
(self.east * precision).ceil / precision,
|
373
|
+
(self.north * precision).ceil / precision
|
374
|
+
].join(',')
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
|
379
|
+
class CoordinateSequence
|
380
|
+
# Returns a Ruby Array of GLatLngs.
|
381
|
+
def to_g_lat_lng(options = {})
|
382
|
+
klass = if options[:short_class]
|
383
|
+
'GLatLng'
|
384
|
+
else
|
385
|
+
'google.maps.LatLng'
|
386
|
+
end
|
387
|
+
|
388
|
+
self.to_a.collect do |p|
|
389
|
+
"new #{klass}(#{p[1]}, #{p[0]})"
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Returns a new GPolyline. Note that this GPolyline just uses whatever
|
394
|
+
# coordinates are found in the sequence in order, so it might not
|
395
|
+
# make much sense at all.
|
396
|
+
#
|
397
|
+
# The options Hash follows the Google Maps API arguments to the
|
398
|
+
# GPolyline constructor and include :color, :weight, :opacity and
|
399
|
+
# :options. 'null' is used in place of any unset options.
|
400
|
+
def to_g_polyline polyline_options = {}, options = {}
|
401
|
+
klass = if options[:short_class]
|
402
|
+
'GPolyline'
|
403
|
+
else
|
404
|
+
'google.maps.Polyline'
|
405
|
+
end
|
406
|
+
|
407
|
+
poly_opts = if polyline_options[:polyline_options]
|
408
|
+
polyline_options[:polyline_options].inject({}) do |memo, (k, v)|
|
409
|
+
memo[Geos::Helper.camelize(k.to_s)] = v
|
410
|
+
memo
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
args = [
|
415
|
+
(polyline_options[:color] ? "'#{Geos::Helper.escape_javascript(polyline_options[:color])}'" : 'null'),
|
416
|
+
(polyline_options[:weight] || 'null'),
|
417
|
+
(polyline_options[:opacity] || 'null'),
|
418
|
+
(poly_opts ? poly_opts.to_json : 'null')
|
419
|
+
].join(', ')
|
420
|
+
|
421
|
+
"new #{klass}([#{self.to_g_lat_lng(options).join(', ')}], #{args})"
|
422
|
+
end
|
423
|
+
|
424
|
+
# Returns a new GPolygon. Note that this GPolygon just uses whatever
|
425
|
+
# coordinates are found in the sequence in order, so it might not
|
426
|
+
# make much sense at all.
|
427
|
+
#
|
428
|
+
# The options Hash follows the Google Maps API arguments to the
|
429
|
+
# GPolygon constructor and include :stroke_color, :stroke_weight,
|
430
|
+
# :stroke_opacity, :fill_color, :fill_opacity and :options. 'null' is
|
431
|
+
# used in place of any unset options.
|
432
|
+
def to_g_polygon polygon_options = {}, options = {}
|
433
|
+
klass = if options[:short_class]
|
434
|
+
'GPolygon'
|
435
|
+
else
|
436
|
+
'google.maps.Polygon'
|
437
|
+
end
|
438
|
+
|
439
|
+
poly_opts = if polygon_options[:polygon_options]
|
440
|
+
polygon_options[:polygon_options].inject({}) do |memo, (k, v)|
|
441
|
+
memo[Geos::Helper.camelize(k.to_s)] = v
|
442
|
+
memo
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
args = [
|
447
|
+
(polygon_options[:stroke_color] ? "'#{Geos::Helper.escape_javascript(polygon_options[:stroke_color])}'" : 'null'),
|
448
|
+
(polygon_options[:stroke_weight] || 'null'),
|
449
|
+
(polygon_options[:stroke_opacity] || 'null'),
|
450
|
+
(polygon_options[:fill_color] ? "'#{Geos::Helper.escape_javascript(polygon_options[:fill_color])}'" : 'null'),
|
451
|
+
(polygon_options[:fill_opacity] || 'null'),
|
452
|
+
(poly_opts ? poly_opts.to_json : 'null')
|
453
|
+
].join(', ')
|
454
|
+
"new #{klass}([#{self.to_g_lat_lng(options).join(', ')}], #{args})"
|
455
|
+
end
|
456
|
+
|
457
|
+
# Returns a Ruby Array of Arrays of coordinates within the
|
458
|
+
# CoordinateSequence in the form [ x, y, z ].
|
459
|
+
def to_a
|
460
|
+
(0...self.length).to_a.collect do |p|
|
461
|
+
[
|
462
|
+
self.get_x(p),
|
463
|
+
(self.dimensions >= 2 ? self.get_y(p) : nil),
|
464
|
+
(self.dimensions >= 3 && self.get_z(p) > 1.7e-306 ? self.get_z(p) : nil)
|
465
|
+
].compact
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Build some XmlMarkup for KML. You can set various KML options like
|
470
|
+
# tessellate, altitudeMode, etc. Use Rails/Ruby-style code and it
|
471
|
+
# will be converted automatically, i.e. :altitudeMode, not
|
472
|
+
# :altitude_mode.
|
473
|
+
def to_kml *args
|
474
|
+
xml, options = Geos::Helper.xml_options(*args)
|
475
|
+
|
476
|
+
xml.LineString(:id => options[:id]) do
|
477
|
+
xml.extrude(options[:extrude]) if options[:extrude]
|
478
|
+
xml.tessellate(options[:tessellate]) if options[:tessellate]
|
479
|
+
xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitudeMode]
|
480
|
+
xml.coordinates do
|
481
|
+
self.to_a.each do
|
482
|
+
xml << (self.to_a.join(','))
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# Build some XmlMarkup for GeoRSS GML. You should include the
|
489
|
+
# appropriate georss and gml XML namespaces in your document.
|
490
|
+
def to_georss *args
|
491
|
+
xml, options = Geos::Helper.xml_options(*args)
|
492
|
+
|
493
|
+
xml.georss(:where) do
|
494
|
+
xml.gml(:LineString) do
|
495
|
+
xml.gml(:posList) do
|
496
|
+
xml << self.to_a.collect do |p|
|
497
|
+
"#{p[1]} #{p[0]}"
|
498
|
+
end.join(' ')
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# Returns a Hash suitable for converting to JSON.
|
505
|
+
#
|
506
|
+
# Options:
|
507
|
+
#
|
508
|
+
# * :encoded - enable or disable Google Maps encoding. The default is
|
509
|
+
# true.
|
510
|
+
# * :level - set the level of the Google Maps encoding algorithm.
|
511
|
+
def to_jsonable options = {}
|
512
|
+
options = {
|
513
|
+
:encoded => true,
|
514
|
+
:level => 3
|
515
|
+
}.merge options
|
516
|
+
|
517
|
+
if options[:encoded]
|
518
|
+
{
|
519
|
+
:type => 'lineString',
|
520
|
+
:encoded => true
|
521
|
+
}.merge(Geos::GoogleMaps::PolylineEncoder.encode(self.to_a, options[:level]))
|
522
|
+
else
|
523
|
+
{
|
524
|
+
:type => 'lineString',
|
525
|
+
:encoded => false,
|
526
|
+
:points => self.to_a
|
527
|
+
}
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
|
533
|
+
class Point
|
534
|
+
# Returns a new GLatLng.
|
535
|
+
def to_g_lat_lng(options = {})
|
536
|
+
klass = if options[:short_class]
|
537
|
+
'GLatLng'
|
538
|
+
else
|
539
|
+
'google.maps.LatLng'
|
540
|
+
end
|
541
|
+
|
542
|
+
"new #{klass}(#{self.lat}, #{self.lng})"
|
543
|
+
end
|
544
|
+
|
545
|
+
# Returns a new GPoint
|
546
|
+
def to_g_point(options = {})
|
547
|
+
klass = if options[:short_class]
|
548
|
+
'GPoint'
|
549
|
+
else
|
550
|
+
'google.maps.Point'
|
551
|
+
end
|
552
|
+
|
553
|
+
"new #{klass}(#{self.x}, #{self.y})"
|
554
|
+
end
|
555
|
+
|
556
|
+
# Returns the Y coordinate of the Point, which is actually the
|
557
|
+
# latitude.
|
558
|
+
def lat
|
559
|
+
self.to_a[1]
|
560
|
+
end
|
561
|
+
alias :latitude :lat
|
562
|
+
alias :y :lat
|
563
|
+
|
564
|
+
# Returns the X coordinate of the Point, which is actually the
|
565
|
+
# longitude.
|
566
|
+
def lng
|
567
|
+
self.to_a[0]
|
568
|
+
end
|
569
|
+
alias :longitude :lng
|
570
|
+
alias :x :lng
|
571
|
+
|
572
|
+
# Returns the Z coordinate of the Point.
|
573
|
+
def z
|
574
|
+
if self.has_z?
|
575
|
+
self.to_a[2]
|
576
|
+
else
|
577
|
+
nil
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# Returns the Point's coordinates as an Array in the following format:
|
582
|
+
#
|
583
|
+
# [ x, y, z ]
|
584
|
+
#
|
585
|
+
# The Z coordinate will only be present for Points which have a Z
|
586
|
+
# dimension.
|
587
|
+
def to_a
|
588
|
+
cs = self.coord_seq
|
589
|
+
@to_a ||= if self.has_z?
|
590
|
+
[ cs.get_x(0), cs.get_y(0), cs.get_z(0) ]
|
591
|
+
else
|
592
|
+
[ cs.get_x(0), cs.get_y(0) ]
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
# Optimize some unnecessary code away:
|
597
|
+
%w{
|
598
|
+
upper_left upper_right lower_right lower_left
|
599
|
+
top bottom right left
|
600
|
+
n s e w
|
601
|
+
ne nw se sw
|
602
|
+
}.each do |name|
|
603
|
+
src, line = <<-EOF, __LINE__ + 1
|
604
|
+
def #{name}
|
605
|
+
self
|
606
|
+
end
|
607
|
+
EOF
|
608
|
+
self.class_eval(src, __FILE__, line)
|
609
|
+
end
|
610
|
+
|
611
|
+
# Build some XmlMarkup for KML. You can set KML options for extrude and
|
612
|
+
# altitudeMode. Use Rails/Ruby-style code and it will be converted
|
613
|
+
# appropriately, i.e. :altitude_mode, not :altitudeMode.
|
614
|
+
def to_kml *args
|
615
|
+
xml, options = Geos::Helper.xml_options(*args)
|
616
|
+
xml.Point(:id => options[:id]) do
|
617
|
+
xml.extrude(options[:extrude]) if options[:extrude]
|
618
|
+
xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitude_mode]
|
619
|
+
xml.coordinates(self.to_a.join(','))
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# Build some XmlMarkup for GeoRSS. You should include the
|
624
|
+
# appropriate georss and gml XML namespaces in your document.
|
625
|
+
def to_georss *args
|
626
|
+
xml, options = Geos::Helper.xml_options(*args)
|
627
|
+
xml.georss(:where) do
|
628
|
+
xml.gml(:Point) do
|
629
|
+
xml.gml(:pos, "#{self.lat} #{self.lng}")
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
# Returns a Hash suitable for converting to JSON.
|
635
|
+
def to_jsonable options = {}
|
636
|
+
cs = self.coord_seq
|
637
|
+
if self.has_z?
|
638
|
+
{ :type => 'point', :lat => cs.get_y(0), :lng => cs.get_x(0), :z => cs.get_z(0) }
|
639
|
+
else
|
640
|
+
{ :type => 'point', :lat => cs.get_y(0), :lng => cs.get_x(0) }
|
641
|
+
end
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
|
646
|
+
class Polygon
|
647
|
+
# Returns a GPolyline of the exterior ring of the Polygon. This does
|
648
|
+
# not take into consideration any interior rings the Polygon may
|
649
|
+
# have.
|
650
|
+
def to_g_polyline polyline_options = {}, options = {}
|
651
|
+
self.exterior_ring.to_g_polyline polyline_options, options
|
652
|
+
end
|
653
|
+
|
654
|
+
# Returns a GPolygon of the exterior ring of the Polygon. This does
|
655
|
+
# not take into consideration any interior rings the Polygon may
|
656
|
+
# have.
|
657
|
+
def to_g_polygon polygon_options = {}, options = {}
|
658
|
+
self.exterior_ring.to_g_polygon polygon_options, options
|
659
|
+
end
|
660
|
+
|
661
|
+
# Build some XmlMarkup for XML. You can set various KML options like
|
662
|
+
# tessellate, altitudeMode, etc. Use Rails/Ruby-style code and it
|
663
|
+
# will be converted automatically, i.e. :altitudeMode, not
|
664
|
+
# :altitude_mode. You can also include interior rings by setting
|
665
|
+
# :interior_rings to true. The default is false.
|
666
|
+
def to_kml *args
|
667
|
+
xml, options = Geos::Helper.xml_options(*args)
|
668
|
+
|
669
|
+
xml.Polygon(:id => options[:id]) do
|
670
|
+
xml.extrude(options[:extrude]) if options[:extrude]
|
671
|
+
xml.tessellate(options[:tessellate]) if options[:tessellate]
|
672
|
+
xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitude_mode]
|
673
|
+
xml.outerBoundaryIs do
|
674
|
+
xml.LinearRing do
|
675
|
+
xml.coordinates do
|
676
|
+
xml << self.exterior_ring.coord_seq.to_a.collect do |p|
|
677
|
+
p.join(',')
|
678
|
+
end.join(' ')
|
679
|
+
end
|
680
|
+
end
|
681
|
+
end
|
682
|
+
(0...self.num_interior_rings).to_a.each do |n|
|
683
|
+
xml.innerBoundaryIs do
|
684
|
+
xml.LinearRing do
|
685
|
+
xml.coordinates do
|
686
|
+
xml << self.interior_ring_n(n).coord_seq.to_a.collect do |p|
|
687
|
+
p.join(',')
|
688
|
+
end.join(' ')
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
end if options[:interior_rings] && self.num_interior_rings > 0
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
# Build some XmlMarkup for GeoRSS. You should include the
|
697
|
+
# appropriate georss and gml XML namespaces in your document.
|
698
|
+
def to_georss *args
|
699
|
+
xml, options = Geos::Helper.xml_options(*args)
|
700
|
+
|
701
|
+
xml.georss(:where) do
|
702
|
+
xml.gml(:Polygon) do
|
703
|
+
xml.gml(:exterior) do
|
704
|
+
xml.gml(:LinearRing) do
|
705
|
+
xml.gml(:posList) do
|
706
|
+
xml << self.exterior_ring.coord_seq.to_a.collect do |p|
|
707
|
+
"#{p[1]} #{p[0]}"
|
708
|
+
end.join(' ')
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
# Returns a Hash suitable for converting to JSON.
|
717
|
+
#
|
718
|
+
# Options:
|
719
|
+
#
|
720
|
+
# * :encoded - enable or disable Google Maps encoding. The default is
|
721
|
+
# true.
|
722
|
+
# * :level - set the level of the Google Maps encoding algorithm.
|
723
|
+
# * :interior_rings - add interior rings to the output. The default
|
724
|
+
# is false.
|
725
|
+
# * :style_options - any style options you want to pass along in the
|
726
|
+
# JSON. These options will be automatically camelized into
|
727
|
+
# Javascripty code.
|
728
|
+
def to_jsonable options = {}
|
729
|
+
options = {
|
730
|
+
:encoded => true,
|
731
|
+
:interior_rings => false
|
732
|
+
}.merge options
|
733
|
+
|
734
|
+
style_options = Hash.new
|
735
|
+
if options[:style_options] && !options[:style_options].empty?
|
736
|
+
options[:style_options].each do |k, v|
|
737
|
+
style_options[Geos::Helper.camelize(k.to_s)] = v
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
if options[:encoded]
|
742
|
+
ret = {
|
743
|
+
:type => 'polygon',
|
744
|
+
:encoded => true,
|
745
|
+
:polylines => [ Geos::GoogleMaps::PolylineEncoder.encode(
|
746
|
+
self.exterior_ring.coord_seq.to_a
|
747
|
+
).merge(:bounds => {
|
748
|
+
:sw => self.lower_left.to_a,
|
749
|
+
:ne => self.upper_right.to_a
|
750
|
+
})
|
751
|
+
],
|
752
|
+
:options => style_options
|
753
|
+
}
|
754
|
+
|
755
|
+
if options[:interior_rings] && self.num_interior_rings > 0
|
756
|
+
(0..(self.num_interior_rings) - 1).to_a.each do |n|
|
757
|
+
ret[:polylines] << Geos::GoogleMaps::PolylineEncoder.encode(self.interior_ring_n(n).coord_seq.to_a)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
ret
|
761
|
+
else
|
762
|
+
ret = {
|
763
|
+
:type => 'polygon',
|
764
|
+
:encoded => false,
|
765
|
+
:polylines => [{
|
766
|
+
:points => self.exterior_ring.coord_seq.to_a,
|
767
|
+
:bounds => {
|
768
|
+
:sw => self.lower_left.to_a,
|
769
|
+
:ne => self.upper_right.to_a
|
770
|
+
}
|
771
|
+
}]
|
772
|
+
}
|
773
|
+
if options[:interior_rings] && self.num_interior_rings > 0
|
774
|
+
(0..(self.num_interior_rings) - 1).to_a.each do |n|
|
775
|
+
ret[:polylines] << {
|
776
|
+
:points => self.interior_ring_n(n).coord_seq.to_a
|
777
|
+
}
|
778
|
+
end
|
779
|
+
end
|
780
|
+
ret
|
781
|
+
end
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
|
786
|
+
class GeometryCollection
|
787
|
+
if !GeometryCollection.included_modules.include?(Enumerable)
|
788
|
+
include Enumerable
|
789
|
+
|
790
|
+
# Iterates the collection through the given block.
|
791
|
+
def each
|
792
|
+
self.num_geometries.times do |n|
|
793
|
+
yield self.get_geometry_n(n)
|
794
|
+
end
|
795
|
+
nil
|
796
|
+
end
|
797
|
+
|
798
|
+
# Returns the nth geometry from the collection.
|
799
|
+
def [](*args)
|
800
|
+
self.to_a[*args]
|
801
|
+
end
|
802
|
+
alias :slice :[]
|
803
|
+
end
|
804
|
+
|
805
|
+
# Returns the last geometry from the collection.
|
806
|
+
def last
|
807
|
+
self.get_geometry_n(self.num_geometries - 1) if self.num_geometries > 0
|
808
|
+
end
|
809
|
+
|
810
|
+
# Returns a Ruby Array of GPolylines for each geometry in the
|
811
|
+
# collection.
|
812
|
+
def to_g_polyline polyline_options = {}, options = {}
|
813
|
+
self.collect do |p|
|
814
|
+
p.to_g_polyline polyline_options, options
|
815
|
+
end
|
816
|
+
end
|
817
|
+
|
818
|
+
# Returns a Ruby Array of GPolygons for each geometry in the
|
819
|
+
# collection.
|
820
|
+
def to_g_polygon polygon_options = {}, options = {}
|
821
|
+
self.collect do |p|
|
822
|
+
p.to_g_polygon polygon_options, options
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
# Returns a Hash suitable for converting to JSON.
|
827
|
+
def to_jsonable options = {}
|
828
|
+
self.collect do |p|
|
829
|
+
p.to_jsonable options
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
# Build some XmlMarkup for KML.
|
834
|
+
def to_kml *args
|
835
|
+
self.collect do |p|
|
836
|
+
p.to_kml(*args)
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
# Build some XmlMarkup for GeoRSS. Since GeoRSS is pretty trimed down,
|
841
|
+
# we just take the entire collection and use the exterior_ring as
|
842
|
+
# a Polygon. Not to bright, mind you, but until GeoRSS stops with the
|
843
|
+
# suck, what are we to do. You should include the appropriate georss
|
844
|
+
# and gml XML namespaces in your document.
|
845
|
+
def to_georss *args
|
846
|
+
self.exterior_ring.to_georss(*args)
|
847
|
+
end
|
848
|
+
end
|
849
|
+
end
|