geos-extensions 0.2.2 → 0.3.0

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