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.
@@ -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