geos-extensions 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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