geos-extensions 0.2.2 → 0.3.0

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.
Files changed (55) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +3 -0
  3. data/Gemfile +17 -0
  4. data/Guardfile +17 -0
  5. data/MIT-LICENSE +1 -1
  6. data/README.rdoc +19 -91
  7. data/Rakefile +1 -12
  8. data/geos-extensions.gemspec +1 -9
  9. data/lib/geos-extensions.rb +1 -9
  10. data/lib/geos/coordinate_sequence.rb +92 -0
  11. data/lib/geos/extensions/version.rb +1 -1
  12. data/lib/geos/geometry.rb +252 -0
  13. data/lib/geos/geometry_collection.rb +60 -0
  14. data/lib/geos/geos_helper.rb +86 -72
  15. data/lib/geos/google_maps.rb +1 -0
  16. data/lib/geos/google_maps/api_2.rb +9 -23
  17. data/lib/geos/google_maps/api_3.rb +10 -24
  18. data/lib/geos/google_maps/api_common.rb +41 -0
  19. data/lib/geos/line_string.rb +15 -0
  20. data/lib/geos/multi_line_string.rb +15 -0
  21. data/lib/geos/multi_point.rb +15 -0
  22. data/lib/geos/multi_polygon.rb +27 -0
  23. data/lib/geos/point.rb +120 -0
  24. data/lib/geos/polygon.rb +158 -0
  25. data/lib/geos/yaml.rb +30 -0
  26. data/lib/geos/yaml/psych.rb +18 -0
  27. data/lib/geos/yaml/syck.rb +41 -0
  28. data/lib/geos_extensions.rb +110 -711
  29. data/test/google_maps_api_2_tests.rb +54 -32
  30. data/test/google_maps_api_3_tests.rb +58 -36
  31. data/test/google_maps_polyline_encoder_tests.rb +1 -1
  32. data/test/helper_tests.rb +28 -0
  33. data/test/misc_tests.rb +130 -10
  34. data/test/reader_tests.rb +38 -1
  35. data/test/test_helper.rb +54 -146
  36. data/test/writer_tests.rb +329 -10
  37. data/test/yaml_tests.rb +203 -0
  38. metadata +26 -102
  39. data/app/models/geos/geometry_column.rb +0 -39
  40. data/app/models/geos/spatial_ref_sys.rb +0 -12
  41. data/lib/geos/active_record_extensions.rb +0 -12
  42. data/lib/geos/active_record_extensions/connection_adapters/postgresql_adapter.rb +0 -151
  43. data/lib/geos/active_record_extensions/spatial_columns.rb +0 -367
  44. data/lib/geos/active_record_extensions/spatial_scopes.rb +0 -493
  45. data/lib/geos/rails/engine.rb +0 -6
  46. data/lib/tasks/test.rake +0 -42
  47. data/test/adapter_tests.rb +0 -38
  48. data/test/database.yml +0 -17
  49. data/test/fixtures/foo3ds.yml +0 -16
  50. data/test/fixtures/foo_geographies.yml +0 -16
  51. data/test/fixtures/foos.yml +0 -16
  52. data/test/geography_columns_tests.rb +0 -176
  53. data/test/geometry_columns_tests.rb +0 -178
  54. data/test/spatial_scopes_geographies_tests.rb +0 -107
  55. data/test/spatial_scopes_tests.rb +0 -337
@@ -0,0 +1,27 @@
1
+
2
+ module Geos
3
+ class MultiPolygon < GeometryCollection
4
+ def to_geojsonable(options = {})
5
+ options = {
6
+ :interior_rings => true
7
+ }.merge(options)
8
+
9
+ {
10
+ :type => 'MultiPolygon',
11
+ :coordinates => self.to_a.collect { |polygon|
12
+ coords = [ polygon.exterior_ring.coord_seq.to_a ]
13
+
14
+ if options[:interior_rings] && polygon.num_interior_rings > 0
15
+ coords.concat polygon.interior_rings.collect { |r|
16
+ r.coord_seq.to_a
17
+ }
18
+ end
19
+
20
+ coords
21
+ }
22
+ }
23
+ end
24
+ alias :as_geojson :to_geojsonable
25
+ end
26
+ end
27
+
@@ -0,0 +1,120 @@
1
+
2
+ module Geos
3
+ class Point
4
+ unless method_defined?(:y)
5
+ # Returns the Y coordinate of the Point.
6
+ def y
7
+ self.to_a[1]
8
+ end
9
+ end
10
+
11
+ %w{
12
+ latitude lat north south n s
13
+ }.each do |name|
14
+ self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
15
+ alias #{name} :y
16
+ EOF
17
+ end
18
+
19
+ unless method_defined?(:x)
20
+ # Returns the X coordinate of the Point.
21
+ def x
22
+ self.to_a[0]
23
+ end
24
+ end
25
+
26
+ %w{
27
+ longitude lng east west e w
28
+ }.each do |name|
29
+ self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
30
+ alias #{name} :x
31
+ EOF
32
+ end
33
+
34
+ unless method_defined?(:z)
35
+ # Returns the Z coordinate of the Point.
36
+ def z
37
+ if self.has_z?
38
+ self.to_a[2]
39
+ else
40
+ nil
41
+ end
42
+ end
43
+ end
44
+
45
+ # Returns the Point's coordinates as an Array in the following format:
46
+ #
47
+ # [ x, y, z ]
48
+ #
49
+ # The Z coordinate will only be present for Points which have a Z
50
+ # dimension.
51
+ def to_a
52
+ if defined?(@to_a)
53
+ @to_a
54
+ else
55
+ cs = self.coord_seq
56
+ @to_a = if self.has_z?
57
+ [ cs.get_x(0), cs.get_y(0), cs.get_z(0) ]
58
+ else
59
+ [ cs.get_x(0), cs.get_y(0) ]
60
+ end
61
+ end
62
+ end
63
+
64
+ # Optimize some unnecessary code away:
65
+ %w{
66
+ upper_left upper_right lower_right lower_left
67
+ ne nw se sw
68
+ northwest northeast southeast southwest
69
+ }.each do |name|
70
+ self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
71
+ def #{name}
72
+ self
73
+ end
74
+ EOF
75
+ end
76
+
77
+ # Build some XmlMarkup for KML. You can set KML options for extrude and
78
+ # altitudeMode. Use Rails/Ruby-style code and it will be converted
79
+ # appropriately, i.e. :altitude_mode, not :altitudeMode.
80
+ def to_kml(*args)
81
+ xml, options = Geos::Helper.xml_options(*args)
82
+ xml.Point(:id => options[:id]) do
83
+ xml.extrude(options[:extrude]) if options[:extrude]
84
+ xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitude_mode]
85
+ xml.coordinates(self.to_a.join(','))
86
+ end
87
+ end
88
+
89
+ # Build some XmlMarkup for GeoRSS. You should include the
90
+ # appropriate georss and gml XML namespaces in your document.
91
+ def to_georss(*args)
92
+ xml = Geos::Helper.xml_options(*args)[0]
93
+ xml.georss(:where) do
94
+ xml.gml(:Point) do
95
+ xml.gml(:pos, "#{self.lat} #{self.lng}")
96
+ end
97
+ end
98
+ end
99
+
100
+ # Returns a Hash suitable for converting to JSON.
101
+ def as_json(options = {})
102
+ cs = self.coord_seq
103
+ if self.has_z?
104
+ { :type => 'point', :lat => cs.get_y(0), :lng => cs.get_x(0), :z => cs.get_z(0) }
105
+ else
106
+ { :type => 'point', :lat => cs.get_y(0), :lng => cs.get_x(0) }
107
+ end
108
+ end
109
+ alias :to_jsonable :as_json
110
+
111
+ def as_geojson(options = {})
112
+ {
113
+ :type => 'Point',
114
+ :coordinates => self.to_a
115
+ }
116
+ end
117
+ alias :to_geojsonable :as_geojson
118
+ end
119
+ end
120
+
@@ -0,0 +1,158 @@
1
+
2
+ module Geos
3
+ class Polygon
4
+ # Build some XmlMarkup for XML. You can set various KML options like
5
+ # tessellate, altitudeMode, etc. Use Rails/Ruby-style code and it
6
+ # will be converted automatically, i.e. :altitudeMode, not
7
+ # :altitude_mode. You can also include interior rings by setting
8
+ # :interior_rings to true. The default is false.
9
+ def to_kml(*args)
10
+ xml, options = Geos::Helper.xml_options(*args)
11
+
12
+ xml.Polygon(:id => options[:id]) do
13
+ xml.extrude(options[:extrude]) if options[:extrude]
14
+ xml.tessellate(options[:tessellate]) if options[:tessellate]
15
+ xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitude_mode]
16
+ xml.outerBoundaryIs do
17
+ xml.LinearRing do
18
+ xml.coordinates do
19
+ xml << self.exterior_ring.coord_seq.to_a.collect do |p|
20
+ p.join(',')
21
+ end.join(' ')
22
+ end
23
+ end
24
+ end
25
+ (0...self.num_interior_rings).to_a.each do |n|
26
+ xml.innerBoundaryIs do
27
+ xml.LinearRing do
28
+ xml.coordinates do
29
+ xml << self.interior_ring_n(n).coord_seq.to_a.collect do |p|
30
+ p.join(',')
31
+ end.join(' ')
32
+ end
33
+ end
34
+ end
35
+ end if options[:interior_rings] && self.num_interior_rings > 0
36
+ end
37
+ end
38
+
39
+ # Build some XmlMarkup for GeoRSS. You should include the
40
+ # appropriate georss and gml XML namespaces in your document.
41
+ def to_georss(*args)
42
+ xml = Geos::Helper.xml_options(*args)[0]
43
+
44
+ xml.georss(:where) do
45
+ xml.gml(:Polygon) do
46
+ xml.gml(:exterior) do
47
+ xml.gml(:LinearRing) do
48
+ xml.gml(:posList) do
49
+ xml << self.exterior_ring.coord_seq.to_a.collect do |p|
50
+ "#{p[1]} #{p[0]}"
51
+ end.join(' ')
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # Returns a Hash suitable for converting to JSON.
60
+ #
61
+ # Options:
62
+ #
63
+ # * :encoded - enable or disable Google Maps encoding. The default is
64
+ # true.
65
+ # * :level - set the level of the Google Maps encoding algorithm.
66
+ # * :interior_rings - add interior rings to the output. The default
67
+ # is false.
68
+ # * :style_options - any style options you want to pass along in the
69
+ # JSON. These options will be automatically camelized into
70
+ # Javascripty code.
71
+ def as_json(options = {})
72
+ options = {
73
+ :encoded => true,
74
+ :level => 3,
75
+ :interior_rings => false
76
+ }.merge options
77
+
78
+ style_options = Hash.new
79
+ if options[:style_options] && !options[:style_options].empty?
80
+ options[:style_options].each do |k, v|
81
+ style_options[Geos::Helper.camelize(k.to_s)] = v
82
+ end
83
+ end
84
+
85
+ if options[:encoded]
86
+ ret = {
87
+ :type => 'polygon',
88
+ :encoded => true,
89
+ :polylines => [ Geos::GoogleMaps::PolylineEncoder.encode(
90
+ self.exterior_ring.coord_seq.to_a,
91
+ options[:level]
92
+ ).merge(:bounds => {
93
+ :sw => self.lower_left.to_a,
94
+ :ne => self.upper_right.to_a
95
+ })
96
+ ],
97
+ :options => style_options
98
+ }
99
+
100
+ if options[:interior_rings] && self.num_interior_rings > 0
101
+ (0..(self.num_interior_rings) - 1).to_a.each do |n|
102
+ ret[:polylines] << Geos::GoogleMaps::PolylineEncoder.encode(
103
+ self.interior_ring_n(n).coord_seq.to_a,
104
+ options[:level]
105
+ )
106
+ end
107
+ end
108
+ ret
109
+ else
110
+ ret = {
111
+ :type => 'polygon',
112
+ :encoded => false,
113
+ :polylines => [{
114
+ :points => self.exterior_ring.coord_seq.to_a,
115
+ :bounds => {
116
+ :sw => self.lower_left.to_a,
117
+ :ne => self.upper_right.to_a
118
+ }
119
+ }]
120
+ }
121
+ if options[:interior_rings] && self.num_interior_rings > 0
122
+ (0..(self.num_interior_rings) - 1).to_a.each do |n|
123
+ ret[:polylines] << {
124
+ :points => self.interior_ring_n(n).coord_seq.to_a
125
+ }
126
+ end
127
+ end
128
+ ret
129
+ end
130
+ end
131
+ alias :to_jsonable :as_json
132
+
133
+ # Options:
134
+ #
135
+ # * :interior_rings - whether to include any interior rings in the output.
136
+ # The default is true.
137
+ def as_geojson(options = {})
138
+ options = {
139
+ :interior_rings => true
140
+ }.merge(options)
141
+
142
+ ret = {
143
+ :type => 'Polygon',
144
+ :coordinates => [ self.exterior_ring.coord_seq.to_a ]
145
+ }
146
+
147
+ if options[:interior_rings] && self.num_interior_rings > 0
148
+ ret[:coordinates].concat self.interior_rings.collect { |r|
149
+ r.coord_seq.to_a
150
+ }
151
+ end
152
+
153
+ ret
154
+ end
155
+ alias :to_geojsonable :as_geojson
156
+ end
157
+ end
158
+
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ # This file adds yaml serialization support to geometries. The generated yaml
4
+ # has this format:
5
+ #
6
+ # !ruby/object:Geos::Geometry
7
+ # geom: SRID=4326; POINT (-104.97 39.71)
8
+ #
9
+ # So to use this in a rails fixture file you could do something like this:
10
+ #
11
+ # geometry_1:
12
+ # id: 1
13
+ # geom: !ruby/object:Geos::Geometry
14
+ # geom: SRID=4326; POINT (-104.97 39.71)
15
+ #
16
+ # Note this code assumes the use of Psych (not syck) and ruby 1.9 and higher
17
+
18
+ require 'yaml'
19
+
20
+ dirname = File.join(File.dirname(__FILE__), 'yaml')
21
+
22
+ # Ruby 2.0 check
23
+ if Object.const_defined?(:Psych) && YAML == Psych
24
+ require File.join(dirname, 'psych')
25
+ # Ruby 1.9 check
26
+ elsif YAML.const_defined?('ENGINE') && YAML::ENGINE.yamler = 'psych'
27
+ require File.join(dirname, 'psych')
28
+ else
29
+ require File.join(dirname, 'syck')
30
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ module Geos
4
+ class Geometry
5
+ def init_with(coder)
6
+ # Convert wkt to a geos pointer
7
+ @ptr = Geos.read(coder['geom']).ptr
8
+ end
9
+
10
+ def encode_with(coder)
11
+ # Note we enforce ASCII encoding so the geom in the YAML file is
12
+ # readable -- otherwise psych converts it to a binary string.
13
+ coder['geom'] = self.to_ewkt(
14
+ :include_srid => self.srid != 0
15
+ ).force_encoding('ASCII')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+
3
+ module Geos
4
+ class Geometry
5
+ yaml_as "tag:ruby.yaml.org,2002:object:Geos::Geometry"
6
+
7
+ def taguri
8
+ "tag:ruby.yaml.org,2002:object:#{self.class.name}"
9
+ end
10
+
11
+ def self.yaml_new(klass, tag, val)
12
+ Geos.read(val['geom'])
13
+ end
14
+
15
+ def to_yaml( opts = {} )
16
+ YAML::quick_emit(self.object_id, opts) do |out|
17
+ out.map(taguri) do |map|
18
+ map.add('geom', self.to_ewkt(
19
+ :include_srid => self.srid != 0
20
+ ))
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ class Point
27
+ yaml_as "tag:ruby.yaml.org,2002:object:Geos::Point"
28
+ end
29
+
30
+ class LineString
31
+ yaml_as "tag:ruby.yaml.org,2002:object:Geos::LineString"
32
+ end
33
+
34
+ class Polygon
35
+ yaml_as "tag:ruby.yaml.org,2002:object:Geos::Polygon"
36
+ end
37
+
38
+ class GeometryCollection
39
+ yaml_as "tag:ruby.yaml.org,2002:object:Geos::GeometryCollection"
40
+ end
41
+ end
@@ -9,14 +9,24 @@ end
9
9
  require 'geos' unless defined?(Geos)
10
10
 
11
11
  require File.join(File.dirname(__FILE__), *%w{ geos extensions version })
12
+ require File.join(File.dirname(__FILE__), *%w{ geos yaml })
12
13
 
13
14
  # Some custom extensions to the SWIG-based Geos Ruby extension.
14
15
  module Geos
15
16
  GEOS_EXTENSIONS_BASE = File.join(File.dirname(__FILE__))
16
17
  GEOS_EXTENSIONS_VERSION = Geos::Extensions::VERSION
17
18
 
19
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos geometry })
20
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos coordinate_sequence })
21
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos point })
22
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos line_string })
23
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos polygon })
24
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos geometry_collection })
25
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos multi_polygon })
26
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos multi_line_string })
27
+ require File.join(GEOS_EXTENSIONS_BASE, *%w{ geos multi_point })
28
+
18
29
  autoload :Helper, File.join(GEOS_EXTENSIONS_BASE, *%w{ geos geos_helper })
19
- autoload :ActiveRecord, File.join(GEOS_EXTENSIONS_BASE, *%w{ geos active_record_extensions })
20
30
  autoload :GoogleMaps, File.join(GEOS_EXTENSIONS_BASE, *%w{ geos google_maps })
21
31
 
22
32
  REGEXP_FLOAT = /(-?\d*(?:\.\d+)?|-?\d*(?:\.\d+?)[eE][-+]?\d+)/
@@ -76,27 +86,67 @@ module Geos
76
86
  geom
77
87
  end
78
88
 
89
+ ALLOWED_GEOS_READ_TYPES = [
90
+ :geometry,
91
+ :wkt,
92
+ :wkb,
93
+ :wkb_hex,
94
+ :g_lat_lng_bounds,
95
+ :g_lat_lng,
96
+ :box2d,
97
+ :wkb,
98
+ :nil
99
+ ]
100
+
79
101
  # Tries its best to return a Geometry object.
80
102
  def self.read(geom, options = {})
81
- geos = case geom
103
+ allowed = Geos::Helper.array_wrap(options[:allowed] || ALLOWED_GEOS_READ_TYPES)
104
+ allowed = allowed - Geos::Helper.array_wrap(options[:excluded])
105
+
106
+ geom = geom.dup.force_encoding('BINARY') if geom.respond_to?(:force_encoding)
107
+
108
+ type = case geom
82
109
  when Geos::Geometry
83
- geom
110
+ :geometry
84
111
  when REGEXP_WKT
85
- Geos.from_wkt(geom, options)
112
+ :wkt
86
113
  when REGEXP_WKB_HEX
87
- Geos.from_wkb(geom, options)
88
- when REGEXP_G_LAT_LNG_BOUNDS, REGEXP_G_LAT_LNG
89
- Geos.from_g_lat_lng(geom, options)
114
+ :wkb_hex
115
+ when REGEXP_G_LAT_LNG_BOUNDS
116
+ :g_lat_lng_bounds
117
+ when REGEXP_G_LAT_LNG
118
+ :g_lat_lng
90
119
  when REGEXP_BOX2D
91
- Geos.from_box2d(geom)
120
+ :box2d
92
121
  when String
93
- Geos.from_wkb(geom.unpack('H*').first.upcase, options)
122
+ :wkb
94
123
  when nil
95
- nil
124
+ :nil
96
125
  else
97
126
  raise ArgumentError.new("Invalid geometry!")
98
127
  end
99
128
 
129
+ if !allowed.include?(type)
130
+ raise ArgumentError.new("geom appears to be a #{type} but #{type} is being filtered")
131
+ end
132
+
133
+ geos = case type
134
+ when :geometry
135
+ geom
136
+ when :wkt
137
+ Geos.from_wkt($~, options)
138
+ when :wkb_hex
139
+ Geos.from_wkb(geom, options)
140
+ when :g_lat_lng_bounds, :g_lat_lng
141
+ Geos.from_g_lat_lng($~, options)
142
+ when :box2d
143
+ Geos.from_box2d($~)
144
+ when :wkb
145
+ Geos.from_wkb(geom.unpack('H*').first.upcase, options)
146
+ when :nil
147
+ nil
148
+ end
149
+
100
150
  if geos && options[:srid]
101
151
  geos.srid = options[:srid]
102
152
  end
@@ -106,8 +156,13 @@ module Geos
106
156
 
107
157
  # Returns some kind of Geometry object from the given WKT. This method
108
158
  # will also accept PostGIS-style EWKT and its various enhancements.
109
- def self.from_wkt(wkt, options = {})
110
- srid, raw_wkt = wkt.scan(REGEXP_WKT).first
159
+ def self.from_wkt(wkt_or_match_data, options = {})
160
+ srid, raw_wkt = if wkt_or_match_data.kind_of?(MatchData)
161
+ [ wkt_or_match_data[1], wkt_or_match_data[2] ]
162
+ else
163
+ wkt_or_match_data.scan(REGEXP_WKT).first
164
+ end
165
+
111
166
  geom = self.wkt_reader_singleton.read(raw_wkt.upcase)
112
167
  geom.srid = (options[:srid] || srid).to_i if options[:srid] || srid
113
168
  geom
@@ -121,729 +176,73 @@ module Geos
121
176
  # while for GLatLngBounds we return a Geos::Polygon that encompasses the
122
177
  # bounds. Use the option :points to interpret the incoming value as
123
178
  # as GPoints rather than GLatLngs.
124
- def self.from_g_lat_lng(geometry, options = {})
125
- geom = case geometry
126
- when REGEXP_G_LAT_LNG_BOUNDS
127
- coords = Array.new
128
- $~.captures.compact.each_slice(2) { |f|
129
- coords << f.collect(&:to_f)
130
- }
131
-
132
- unless options[:points]
133
- coords.each do |c|
134
- c.reverse!
135
- end
136
- end
137
-
138
- Geos.from_wkt("LINESTRING(%s, %s)" % [
139
- coords[0].join(' '),
140
- coords[1].join(' ')
141
- ]).envelope
142
- when REGEXP_G_LAT_LNG
143
- coords = $~.captures.collect(&:to_f).tap { |c|
144
- c.reverse! unless options[:points]
145
- }
146
- Geos.from_wkt("POINT(#{coords.join(' ')})")
179
+ def self.from_g_lat_lng(geometry_or_match_data, options = {})
180
+ match_data = case geometry_or_match_data
181
+ when MatchData
182
+ geometry_or_match_data.captures
183
+ when REGEXP_G_LAT_LNG_BOUNDS, REGEXP_G_LAT_LNG
184
+ $~.captures
147
185
  else
148
186
  raise "Invalid GLatLng format"
149
187
  end
150
188
 
151
- if options[:srid]
152
- geom.srid = options[:srid]
153
- end
154
-
155
- geom
156
- end
157
-
158
- # Same as from_g_lat_lng but uses GPoints instead of GLatLngs and GBounds
159
- # instead of GLatLngBounds. Equivalent to calling from_g_lat_lng with a
160
- # non-false expression for the points parameter.
161
- def self.from_g_point(geometry, options = {})
162
- self.from_g_lat_lng(geometry, options.merge(:points => true))
163
- end
164
-
165
- # Creates a Geometry from a PostGIS-style BOX string.
166
- def self.from_box2d(geometry)
167
- if geometry =~ REGEXP_BOX2D
168
- coords = []
169
- $~.captures.compact.each_slice(2) { |f|
189
+ geom = if match_data.length > 3
190
+ coords = Array.new
191
+ match_data.compact.each_slice(2) { |f|
170
192
  coords << f.collect(&:to_f)
171
193
  }
172
194
 
195
+ unless options[:points]
196
+ coords.each do |c|
197
+ c.reverse!
198
+ end
199
+ end
200
+
173
201
  Geos.from_wkt("LINESTRING(%s, %s)" % [
174
202
  coords[0].join(' '),
175
203
  coords[1].join(' ')
176
204
  ]).envelope
177
205
  else
178
- raise "Invalid BOX2D"
179
- end
180
- end
181
-
182
- # This is our base module that we use for some generic methods used all
183
- # over the place.
184
- class Geometry
185
- protected
186
-
187
- WKB_WRITER_OPTIONS = [ :output_dimensions, :byte_order, :include_srid ].freeze
188
- def wkb_writer(options = {}) #:nodoc:
189
- writer = WkbWriter.new
190
- options.reject { |k, v| !WKB_WRITER_OPTIONS.include?(k) }.each do |k, v|
191
- writer.send("#{k}=", v)
192
- end
193
- writer
194
- end
195
-
196
- public
197
-
198
- # Spits the geometry out into WKB in binary.
199
- #
200
- # You can set the :output_dimensions, :byte_order and :include_srid
201
- # options via the options Hash.
202
- def to_wkb_bin(options = {})
203
- wkb_writer(options).write(self)
204
- end
205
-
206
- # Quickly call to_wkb_bin with :include_srid set to true.
207
- def to_ewkb_bin(options = {})
208
- options = {
209
- :include_srid => true
210
- }.merge options
211
- to_wkb_bin(options)
212
- end
213
-
214
- # Spits the geometry out into WKB in hex.
215
- #
216
- # You can set the :output_dimensions, :byte_order and :include_srid
217
- # options via the options Hash.
218
- def to_wkb(options = {})
219
- wkb_writer(options).write_hex(self)
220
- end
221
-
222
- # Quickly call to_wkb with :include_srid set to true.
223
- def to_ewkb(options = {})
224
- options = {
225
- :include_srid => true
226
- }.merge options
227
- to_wkb(options)
228
- end
229
-
230
- # Spits the geometry out into WKT. You can specify the :include_srid
231
- # option to create a PostGIS-style EWKT output.
232
- def to_wkt(options = {})
233
- writer = WktWriter.new
234
-
235
- # Older versions of the Geos library don't allow for options here.
236
- args = if WktWriter.instance_method(:write).arity < -1
237
- [ options ]
238
- else
239
- []
240
- end
241
-
242
- ret = ''
243
-
244
- if options[:include_srid]
245
- srid = if options[:srid]
246
- options[:srid]
247
- else
248
- self.srid
249
- end
250
-
251
- ret << "SRID=#{srid};"
252
- end
253
-
254
- ret << writer.write(self, *args)
255
- ret
256
- end
257
-
258
- # Quickly call to_wkt with :include_srid set to true.
259
- def to_ewkt(options = {})
260
- options = {
261
- :include_srid => true
262
- }.merge options
263
- to_wkt(options)
264
- end
265
-
266
- # Returns a Point for the envelope's upper left coordinate.
267
- def upper_left
268
- if defined?(@upper_left)
269
- @upper_left
270
- else
271
- cs = self.envelope.exterior_ring.coord_seq
272
- @upper_left = Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(3)} #{cs.get_y(3)})")
273
- end
274
- end
275
- alias :nw :upper_left
276
- alias :northwest :upper_left
277
-
278
- # Returns a Point for the envelope's upper right coordinate.
279
- def upper_right
280
- if defined?(@upper_right)
281
- @upper_right
282
- else
283
- cs = self.envelope.exterior_ring.coord_seq
284
- @upper_right = Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(2)} #{cs.get_y(2)})")
285
- end
286
- end
287
- alias :ne :upper_right
288
- alias :northeast :upper_right
289
-
290
- # Returns a Point for the envelope's lower right coordinate.
291
- def lower_right
292
- if defined?(@lower_right)
293
- @lower_right
294
- else
295
- cs = self.envelope.exterior_ring.coord_seq
296
- @lower_right = Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(1)} #{cs.get_y(1)})")
297
- end
298
- end
299
- alias :se :lower_right
300
- alias :southeast :lower_right
301
-
302
- # Returns a Point for the envelope's lower left coordinate.
303
- def lower_left
304
- if defined?(@lower_left)
305
- @lower_left
306
- else
307
- cs = self.envelope.exterior_ring.coord_seq
308
- @lower_left = Geos::wkt_reader_singleton.read("POINT(#{cs.get_x(0)} #{cs.get_y(0)})")
309
- end
310
- end
311
- alias :sw :lower_left
312
- alias :southwest :lower_left
313
-
314
- # Northern-most Y coordinate.
315
- def top
316
- if defined?(@top)
317
- @top
318
- else
319
- @top = self.upper_right.y
320
- end
321
- end
322
- alias :n :top
323
- alias :north :top
324
-
325
- # Eastern-most X coordinate.
326
- def right
327
- if defined?(@right)
328
- @right
329
- else
330
- @right = self.upper_right.x
331
- end
332
- end
333
- alias :e :right
334
- alias :east :right
335
-
336
- # Southern-most Y coordinate.
337
- def bottom
338
- if defined?(@bottom)
339
- @bottom
340
- else
341
- @bottom = self.lower_left.y
342
- end
343
- end
344
- alias :s :bottom
345
- alias :south :bottom
346
-
347
- # Western-most X coordinate.
348
- def left
349
- if defined?(@left)
350
- @left
351
- else
352
- @left = self.lower_left.x
353
- end
354
- end
355
- alias :w :left
356
- alias :west :left
357
-
358
- # Spits out a bounding box the way Flickr likes it. You can set the
359
- # precision of the rounding using the :precision option. In order to
360
- # ensure that the box is indeed a box and not merely a point, the
361
- # southwest coordinates are floored and the northeast point ceiled.
362
- def to_flickr_bbox(options = {})
363
- options = {
364
- :precision => 1
365
- }.merge(options)
366
- precision = 10.0 ** options[:precision]
367
-
368
- [
369
- (self.west * precision).floor / precision,
370
- (self.south * precision).floor / precision,
371
- (self.east * precision).ceil / precision,
372
- (self.north * precision).ceil / precision
373
- ].join(',')
374
- end
375
-
376
- def to_geojson(options = {})
377
- self.to_geojsonable(options).to_json
378
- end
379
- end
380
-
381
-
382
- class CoordinateSequence
383
- # Returns a Ruby Array of Arrays of coordinates within the
384
- # CoordinateSequence in the form [ x, y, z ].
385
- def to_a
386
- (0...self.length).to_a.collect do |p|
387
- [
388
- self.get_x(p),
389
- (self.dimensions >= 2 ? self.get_y(p) : nil),
390
- (self.dimensions >= 3 && self.get_z(p) > 1.7e-306 ? self.get_z(p) : nil)
391
- ].compact
392
- end
393
- end
394
-
395
- # Build some XmlMarkup for KML. You can set various KML options like
396
- # tessellate, altitudeMode, etc. Use Rails/Ruby-style code and it
397
- # will be converted automatically, i.e. :altitudeMode, not
398
- # :altitude_mode.
399
- def to_kml *args
400
- xml, options = Geos::Helper.xml_options(*args)
401
-
402
- xml.LineString(:id => options[:id]) do
403
- xml.extrude(options[:extrude]) if options[:extrude]
404
- xml.tessellate(options[:tessellate]) if options[:tessellate]
405
- xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitudeMode]
406
- xml.coordinates do
407
- self.to_a.each do
408
- xml << (self.to_a.join(','))
409
- end
410
- end
411
- end
412
- end
413
-
414
- # Build some XmlMarkup for GeoRSS GML. You should include the
415
- # appropriate georss and gml XML namespaces in your document.
416
- def to_georss *args
417
- xml = Geos::Helper.xml_options(*args)[0]
418
-
419
- xml.georss(:where) do
420
- xml.gml(:LineString) do
421
- xml.gml(:posList) do
422
- xml << self.to_a.collect do |p|
423
- "#{p[1]} #{p[0]}"
424
- end.join(' ')
425
- end
426
- end
427
- end
428
- end
429
-
430
- # Returns a Hash suitable for converting to JSON.
431
- #
432
- # Options:
433
- #
434
- # * :encoded - enable or disable Google Maps encoding. The default is
435
- # true.
436
- # * :level - set the level of the Google Maps encoding algorithm.
437
- def to_jsonable options = {}
438
- options = {
439
- :encoded => true,
440
- :level => 3
441
- }.merge options
442
-
443
- if options[:encoded]
444
- {
445
- :type => 'lineString',
446
- :encoded => true
447
- }.merge(Geos::GoogleMaps::PolylineEncoder.encode(self.to_a, options[:level]))
448
- else
449
- {
450
- :type => 'lineString',
451
- :encoded => false,
452
- :points => self.to_a
453
- }
454
- end
455
- end
456
-
457
- def to_geojsonable(options = {})
458
- {
459
- :type => 'LineString',
460
- :coordinates => self.to_a
206
+ coords = match_data.collect(&:to_f).tap { |c|
207
+ c.reverse! unless options[:points]
461
208
  }
209
+ Geos.from_wkt("POINT(#{coords.join(' ')})")
462
210
  end
463
211
 
464
- def to_geojson(options = {})
465
- self.to_geojsonable(options).to_json
466
- end
467
- end
468
-
469
-
470
- class Point
471
- unless method_defined?(:y)
472
- # Returns the Y coordinate of the Point.
473
- def y
474
- self.to_a[1]
475
- end
476
- end
477
-
478
- %w{
479
- latitude lat north south n s
480
- }.each do |name|
481
- self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
482
- alias #{name} :y
483
- EOF
484
- end
485
-
486
- unless method_defined?(:x)
487
- # Returns the X coordinate of the Point.
488
- def x
489
- self.to_a[0]
490
- end
491
- end
492
-
493
- %w{
494
- longitude lng east west e w
495
- }.each do |name|
496
- self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
497
- alias #{name} :x
498
- EOF
499
- end
500
-
501
- unless method_defined?(:z)
502
- # Returns the Z coordinate of the Point.
503
- def z
504
- if self.has_z?
505
- self.to_a[2]
506
- else
507
- nil
508
- end
509
- end
510
- end
511
-
512
- # Returns the Point's coordinates as an Array in the following format:
513
- #
514
- # [ x, y, z ]
515
- #
516
- # The Z coordinate will only be present for Points which have a Z
517
- # dimension.
518
- def to_a
519
- if defined?(@to_a)
520
- @to_a
521
- else
522
- cs = self.coord_seq
523
- @to_a = if self.has_z?
524
- [ cs.get_x(0), cs.get_y(0), cs.get_z(0) ]
525
- else
526
- [ cs.get_x(0), cs.get_y(0) ]
527
- end
528
- end
529
- end
530
-
531
- # Optimize some unnecessary code away:
532
- %w{
533
- upper_left upper_right lower_right lower_left
534
- ne nw se sw
535
- northwest northeast southeast southwest
536
- }.each do |name|
537
- self.class_eval(<<-EOF, __FILE__, __LINE__ + 1)
538
- def #{name}
539
- self
540
- end
541
- EOF
542
- end
543
-
544
- # Build some XmlMarkup for KML. You can set KML options for extrude and
545
- # altitudeMode. Use Rails/Ruby-style code and it will be converted
546
- # appropriately, i.e. :altitude_mode, not :altitudeMode.
547
- def to_kml *args
548
- xml, options = Geos::Helper.xml_options(*args)
549
- xml.Point(:id => options[:id]) do
550
- xml.extrude(options[:extrude]) if options[:extrude]
551
- xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitude_mode]
552
- xml.coordinates(self.to_a.join(','))
553
- end
554
- end
555
-
556
- # Build some XmlMarkup for GeoRSS. You should include the
557
- # appropriate georss and gml XML namespaces in your document.
558
- def to_georss *args
559
- xml = Geos::Helper.xml_options(*args)[0]
560
- xml.georss(:where) do
561
- xml.gml(:Point) do
562
- xml.gml(:pos, "#{self.lat} #{self.lng}")
563
- end
564
- end
565
- end
566
-
567
- # Returns a Hash suitable for converting to JSON.
568
- def to_jsonable options = {}
569
- cs = self.coord_seq
570
- if self.has_z?
571
- { :type => 'point', :lat => cs.get_y(0), :lng => cs.get_x(0), :z => cs.get_z(0) }
572
- else
573
- { :type => 'point', :lat => cs.get_y(0), :lng => cs.get_x(0) }
574
- end
212
+ if options[:srid]
213
+ geom.srid = options[:srid]
575
214
  end
576
215
 
577
- def to_geojsonable(options = {})
578
- {
579
- :type => 'Point',
580
- :coordinates => self.to_a
581
- }
582
- end
216
+ geom
583
217
  end
584
218
 
585
-
586
- class LineString
587
- def to_jsonable(options = {})
588
- self.coord_seq.to_jsonable(options)
589
- end
590
-
591
- def to_geojsonable(options = {})
592
- self.coord_seq.to_geojsonable(options)
593
- end
219
+ # Same as from_g_lat_lng but uses GPoints instead of GLatLngs and GBounds
220
+ # instead of GLatLngBounds. Equivalent to calling from_g_lat_lng with a
221
+ # non-false expression for the points parameter.
222
+ def self.from_g_point(geometry, options = {})
223
+ self.from_g_lat_lng(geometry, options.merge(:points => true))
594
224
  end
595
225
 
596
- class Polygon
597
- # Build some XmlMarkup for XML. You can set various KML options like
598
- # tessellate, altitudeMode, etc. Use Rails/Ruby-style code and it
599
- # will be converted automatically, i.e. :altitudeMode, not
600
- # :altitude_mode. You can also include interior rings by setting
601
- # :interior_rings to true. The default is false.
602
- def to_kml *args
603
- xml, options = Geos::Helper.xml_options(*args)
604
-
605
- xml.Polygon(:id => options[:id]) do
606
- xml.extrude(options[:extrude]) if options[:extrude]
607
- xml.tessellate(options[:tessellate]) if options[:tessellate]
608
- xml.altitudeMode(Geos::Helper.camelize(options[:altitude_mode])) if options[:altitude_mode]
609
- xml.outerBoundaryIs do
610
- xml.LinearRing do
611
- xml.coordinates do
612
- xml << self.exterior_ring.coord_seq.to_a.collect do |p|
613
- p.join(',')
614
- end.join(' ')
615
- end
616
- end
617
- end
618
- (0...self.num_interior_rings).to_a.each do |n|
619
- xml.innerBoundaryIs do
620
- xml.LinearRing do
621
- xml.coordinates do
622
- xml << self.interior_ring_n(n).coord_seq.to_a.collect do |p|
623
- p.join(',')
624
- end.join(' ')
625
- end
626
- end
627
- end
628
- end if options[:interior_rings] && self.num_interior_rings > 0
629
- end
630
- end
631
-
632
- # Build some XmlMarkup for GeoRSS. You should include the
633
- # appropriate georss and gml XML namespaces in your document.
634
- def to_georss *args
635
- xml = Geos::Helper.xml_options(*args)[0]
636
-
637
- xml.georss(:where) do
638
- xml.gml(:Polygon) do
639
- xml.gml(:exterior) do
640
- xml.gml(:LinearRing) do
641
- xml.gml(:posList) do
642
- xml << self.exterior_ring.coord_seq.to_a.collect do |p|
643
- "#{p[1]} #{p[0]}"
644
- end.join(' ')
645
- end
646
- end
647
- end
648
- end
649
- end
650
- end
651
-
652
- # Returns a Hash suitable for converting to JSON.
653
- #
654
- # Options:
655
- #
656
- # * :encoded - enable or disable Google Maps encoding. The default is
657
- # true.
658
- # * :level - set the level of the Google Maps encoding algorithm.
659
- # * :interior_rings - add interior rings to the output. The default
660
- # is false.
661
- # * :style_options - any style options you want to pass along in the
662
- # JSON. These options will be automatically camelized into
663
- # Javascripty code.
664
- def to_jsonable options = {}
665
- options = {
666
- :encoded => true,
667
- :level => 3,
668
- :interior_rings => false
669
- }.merge options
670
-
671
- style_options = Hash.new
672
- if options[:style_options] && !options[:style_options].empty?
673
- options[:style_options].each do |k, v|
674
- style_options[Geos::Helper.camelize(k.to_s)] = v
675
- end
676
- end
677
-
678
- if options[:encoded]
679
- ret = {
680
- :type => 'polygon',
681
- :encoded => true,
682
- :polylines => [ Geos::GoogleMaps::PolylineEncoder.encode(
683
- self.exterior_ring.coord_seq.to_a,
684
- options[:level]
685
- ).merge(:bounds => {
686
- :sw => self.lower_left.to_a,
687
- :ne => self.upper_right.to_a
688
- })
689
- ],
690
- :options => style_options
691
- }
692
-
693
- if options[:interior_rings] && self.num_interior_rings > 0
694
- (0..(self.num_interior_rings) - 1).to_a.each do |n|
695
- ret[:polylines] << Geos::GoogleMaps::PolylineEncoder.encode(
696
- self.interior_ring_n(n).coord_seq.to_a,
697
- options[:level]
698
- )
699
- end
700
- end
701
- ret
226
+ # Creates a Geometry from a PostGIS-style BOX string.
227
+ def self.from_box2d(geometry_or_match_data)
228
+ match_data = case geometry_or_match_data
229
+ when MatchData
230
+ geometry_or_match_data.captures
231
+ when REGEXP_BOX2D
232
+ $~.captures
702
233
  else
703
- ret = {
704
- :type => 'polygon',
705
- :encoded => false,
706
- :polylines => [{
707
- :points => self.exterior_ring.coord_seq.to_a,
708
- :bounds => {
709
- :sw => self.lower_left.to_a,
710
- :ne => self.upper_right.to_a
711
- }
712
- }]
713
- }
714
- if options[:interior_rings] && self.num_interior_rings > 0
715
- (0..(self.num_interior_rings) - 1).to_a.each do |n|
716
- ret[:polylines] << {
717
- :points => self.interior_ring_n(n).coord_seq.to_a
718
- }
719
- end
720
- end
721
- ret
722
- end
234
+ raise "Invalid BOX2D"
723
235
  end
724
236
 
725
- # Options:
726
- #
727
- # * :interior_rings - whether to include any interior rings in the output.
728
- # The default is true.
729
- def to_geojsonable(options = {})
730
- options = {
731
- :interior_rings => true
732
- }.merge(options)
733
-
734
- ret = {
735
- :type => 'Polygon',
736
- :coordinates => [ self.exterior_ring.coord_seq.to_a ]
737
- }
237
+ coords = []
238
+ match_data.compact.each_slice(2) { |f|
239
+ coords << f.collect(&:to_f)
240
+ }
738
241
 
739
- if options[:interior_rings] && self.num_interior_rings > 0
740
- ret[:coordinates].concat self.interior_rings.collect { |r|
741
- r.coord_seq.to_a
742
- }
743
- end
744
-
745
- ret
746
- end
747
- end
748
-
749
-
750
- class GeometryCollection
751
- if !GeometryCollection.included_modules.include?(Enumerable)
752
- include Enumerable
753
-
754
- # Iterates the collection through the given block.
755
- def each
756
- self.num_geometries.times do |n|
757
- yield self.get_geometry_n(n)
758
- end
759
- nil
760
- end
761
-
762
- # Returns the nth geometry from the collection.
763
- def [](*args)
764
- self.to_a[*args]
765
- end
766
- alias :slice :[]
767
- end
768
-
769
- # Returns the last geometry from the collection.
770
- def last
771
- self.get_geometry_n(self.num_geometries - 1) if self.num_geometries > 0
772
- end
773
-
774
- # Returns a Hash suitable for converting to JSON.
775
- def to_jsonable options = {}
776
- self.collect do |p|
777
- p.to_jsonable options
778
- end
779
- end
780
-
781
- # Build some XmlMarkup for KML.
782
- def to_kml *args
783
- self.collect do |p|
784
- p.to_kml(*args)
785
- end
786
- end
787
-
788
- # Build some XmlMarkup for GeoRSS. Since GeoRSS is pretty trimed down,
789
- # we just take the entire collection and use the exterior_ring as
790
- # a Polygon. Not to bright, mind you, but until GeoRSS stops with the
791
- # suck, what are we to do. You should include the appropriate georss
792
- # and gml XML namespaces in your document.
793
- def to_georss *args
794
- self.exterior_ring.to_georss(*args)
795
- end
796
-
797
- def to_geojsonable(options = {})
798
- {
799
- :type => 'GeometryCollection',
800
- :geometries => self.to_a.collect { |g| g.to_geojsonable(options) }
801
- }
802
- end
803
- end
804
-
805
- class MultiPolygon < GeometryCollection
806
- def to_geojsonable(options = {})
807
- options = {
808
- :interior_rings => true
809
- }.merge(options)
810
-
811
- {
812
- :type => 'MultiPolygon',
813
- :coordinates => self.to_a.collect { |polygon|
814
- coords = [ polygon.exterior_ring.coord_seq.to_a ]
815
-
816
- if options[:interior_rings] && polygon.num_interior_rings > 0
817
- coords.concat polygon.interior_rings.collect { |r|
818
- r.coord_seq.to_a
819
- }
820
- end
821
-
822
- coords
823
- }
824
- }
825
- end
826
- end
827
-
828
- class MultiLineString < GeometryCollection
829
- def to_geojsonable(options = {})
830
- {
831
- :type => 'MultiLineString',
832
- :coordinates => self.to_a.collect { |linestring|
833
- linestring.coord_seq.to_a
834
- }
835
- }
836
- end
837
- end
838
-
839
- class MultiPoint < GeometryCollection
840
- def to_geojsonable(options = {})
841
- {
842
- :type => 'MultiPoint',
843
- :coordinates => self.to_a.collect { |point|
844
- point.to_a
845
- }
846
- }
847
- end
242
+ Geos.from_wkt("LINESTRING(%s, %s)" % [
243
+ coords[0].join(' '),
244
+ coords[1].join(' ')
245
+ ]).envelope
848
246
  end
849
247
  end
248
+