georuby 1.9.3

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 (75) hide show
  1. data/Gemfile +8 -0
  2. data/Gemfile.lock +29 -0
  3. data/History.txt +4 -0
  4. data/LICENSE +21 -0
  5. data/README.rdoc +184 -0
  6. data/Rakefile +48 -0
  7. data/VERSION +1 -0
  8. data/georuby.gemspec +128 -0
  9. data/lib/geo_ruby.rb +23 -0
  10. data/lib/geo_ruby/geojson.rb +129 -0
  11. data/lib/geo_ruby/georss.rb +133 -0
  12. data/lib/geo_ruby/gpx.rb +1 -0
  13. data/lib/geo_ruby/gpx4r/gpx.rb +118 -0
  14. data/lib/geo_ruby/shp.rb +1 -0
  15. data/lib/geo_ruby/shp4r/dbf.rb +42 -0
  16. data/lib/geo_ruby/shp4r/shp.rb +718 -0
  17. data/lib/geo_ruby/simple_features/envelope.rb +167 -0
  18. data/lib/geo_ruby/simple_features/ewkb_parser.rb +218 -0
  19. data/lib/geo_ruby/simple_features/ewkt_parser.rb +336 -0
  20. data/lib/geo_ruby/simple_features/geometry.rb +236 -0
  21. data/lib/geo_ruby/simple_features/geometry_collection.rb +144 -0
  22. data/lib/geo_ruby/simple_features/geometry_factory.rb +81 -0
  23. data/lib/geo_ruby/simple_features/helper.rb +18 -0
  24. data/lib/geo_ruby/simple_features/line_string.rb +228 -0
  25. data/lib/geo_ruby/simple_features/linear_ring.rb +34 -0
  26. data/lib/geo_ruby/simple_features/multi_line_string.rb +63 -0
  27. data/lib/geo_ruby/simple_features/multi_point.rb +58 -0
  28. data/lib/geo_ruby/simple_features/multi_polygon.rb +64 -0
  29. data/lib/geo_ruby/simple_features/point.rb +381 -0
  30. data/lib/geo_ruby/simple_features/polygon.rb +175 -0
  31. data/nofxx-georuby.gemspec +149 -0
  32. data/spec/data/geojson/feature_collection.json +34 -0
  33. data/spec/data/georss/atom.xml +21 -0
  34. data/spec/data/georss/gml.xml +40 -0
  35. data/spec/data/georss/w3c.xml +22 -0
  36. data/spec/data/gpx/fells_loop.gpx +1077 -0
  37. data/spec/data/gpx/long.gpx +1642 -0
  38. data/spec/data/gpx/long.kml +31590 -0
  39. data/spec/data/gpx/long.nmea +2220 -0
  40. data/spec/data/gpx/short.gpx +13634 -0
  41. data/spec/data/gpx/short.kml +130 -0
  42. data/spec/data/gpx/tracktreks.gpx +706 -0
  43. data/spec/data/multipoint.dbf +0 -0
  44. data/spec/data/multipoint.shp +0 -0
  45. data/spec/data/multipoint.shx +0 -0
  46. data/spec/data/point.dbf +0 -0
  47. data/spec/data/point.shp +0 -0
  48. data/spec/data/point.shx +0 -0
  49. data/spec/data/polygon.dbf +0 -0
  50. data/spec/data/polygon.shp +0 -0
  51. data/spec/data/polygon.shx +0 -0
  52. data/spec/data/polyline.dbf +0 -0
  53. data/spec/data/polyline.shp +0 -0
  54. data/spec/data/polyline.shx +0 -0
  55. data/spec/geo_ruby/geojson_spec.rb +147 -0
  56. data/spec/geo_ruby/georss.rb +218 -0
  57. data/spec/geo_ruby/georss_spec.rb +14 -0
  58. data/spec/geo_ruby/gpx4r/gpx_spec.rb +106 -0
  59. data/spec/geo_ruby/shp4r/shp_spec.rb +239 -0
  60. data/spec/geo_ruby/simple_features/envelope_spec.rb +47 -0
  61. data/spec/geo_ruby/simple_features/ewkb_parser_spec.rb +158 -0
  62. data/spec/geo_ruby/simple_features/ewkt_parser_spec.rb +179 -0
  63. data/spec/geo_ruby/simple_features/geometry_collection_spec.rb +55 -0
  64. data/spec/geo_ruby/simple_features/geometry_factory_spec.rb +11 -0
  65. data/spec/geo_ruby/simple_features/geometry_spec.rb +32 -0
  66. data/spec/geo_ruby/simple_features/line_string_spec.rb +259 -0
  67. data/spec/geo_ruby/simple_features/linear_ring_spec.rb +24 -0
  68. data/spec/geo_ruby/simple_features/multi_line_string_spec.rb +54 -0
  69. data/spec/geo_ruby/simple_features/multi_point_spec.rb +35 -0
  70. data/spec/geo_ruby/simple_features/multi_polygon_spec.rb +50 -0
  71. data/spec/geo_ruby/simple_features/point_spec.rb +356 -0
  72. data/spec/geo_ruby/simple_features/polygon_spec.rb +122 -0
  73. data/spec/geo_ruby_spec.rb +27 -0
  74. data/spec/spec_helper.rb +73 -0
  75. metadata +228 -0
@@ -0,0 +1,236 @@
1
+ module GeoRuby#:nodoc:
2
+ module SimpleFeatures
3
+ #arbitrary default SRID
4
+ DEFAULT_SRID = 4326 unless defined? DEFAULT_SRID
5
+
6
+
7
+ #Root of all geometric data classes.
8
+ #Objects of class Geometry should not be instantiated.
9
+ class Geometry
10
+ #SRID of the geometry
11
+ attr_reader :srid #writer defined below
12
+ #Flag indicating if the z ordinate of the geometry is meaningful
13
+ attr_accessor :with_z
14
+ alias :with_z? :with_z
15
+ #Flag indicating if the m ordinate of the geometry is meaningful
16
+ attr_accessor :with_m
17
+ alias :with_m? :with_m
18
+
19
+ def initialize(srid=DEFAULT_SRID,with_z=false,with_m=false)
20
+ @srid=srid
21
+ @with_z=with_z
22
+ @with_m=with_m
23
+ end
24
+
25
+ def srid=(new_srid)
26
+ @srid = new_srid
27
+ unless self.is_a?(Point)
28
+ self.each do |geom|
29
+ geom.srid=new_srid
30
+ end
31
+ end
32
+ end
33
+
34
+ #to be implemented in subclasses
35
+ def bounding_box
36
+ end
37
+
38
+ #to be implemented in subclasses
39
+ def m_range
40
+ end
41
+
42
+ #Returns an Envelope object for the geometry
43
+ def envelope
44
+ Envelope.from_points(bounding_box,srid,with_z)
45
+ end
46
+
47
+ #Outputs the geometry as an EWKB string.
48
+ #The +allow_srid+, +allow_z+ and +allow_m+ arguments allow the output to include srid, z and m respectively if they are present in the geometry. If these arguments are set to false, srid, z and m are not included, even if they are present in the geometry. By default, the output string contains all the information in the object.
49
+ def as_ewkb(allow_srid=true,allow_z=true,allow_m=true)
50
+ ewkb="";
51
+
52
+ ewkb << 1.chr #little_endian by default
53
+
54
+ type= binary_geometry_type
55
+ if @with_z and allow_z
56
+ type = type | Z_MASK
57
+ end
58
+ if @with_m and allow_m
59
+ type = type | M_MASK
60
+ end
61
+ if allow_srid
62
+ type = type | SRID_MASK
63
+ ewkb << [type,@srid].pack("VV")
64
+ else
65
+ ewkb << [type].pack("V")
66
+ end
67
+
68
+ ewkb << binary_representation(allow_z,allow_m)
69
+ end
70
+
71
+ #Outputs the geometry as a strict WKB string.
72
+ def as_wkb
73
+ as_ewkb(false,false,false)
74
+ end
75
+
76
+ #Outputs the geometry as a HexEWKB string. It is almost the same as a WKB string, except that each byte of a WKB string is replaced by its hexadecimal 2-character representation in a HexEWKB string.
77
+ def as_hex_ewkb(allow_srid=true,allow_z=true,allow_m=true)
78
+ as_ewkb(allow_srid, allow_z, allow_m).unpack('H*').join('').upcase
79
+ end
80
+
81
+ #Outputs the geometry as a strict HexWKB string
82
+ def as_hex_wkb
83
+ as_hex_ewkb(false,false,false)
84
+ end
85
+
86
+ #Outputs the geometry as an EWKT string.
87
+ def as_ewkt(allow_srid=true,allow_z=true,allow_m=true)
88
+ if allow_srid
89
+ ewkt="SRID=#{@srid};"
90
+ else
91
+ ewkt=""
92
+ end
93
+ ewkt << text_geometry_type
94
+ ewkt << "M" if @with_m and allow_m and (!@with_z or !allow_z) #to distinguish the M from the Z when there is actually no Z...
95
+ ewkt << "(" << text_representation(allow_z,allow_m) << ")"
96
+ end
97
+
98
+ #Outputs the geometry as strict WKT string.
99
+ def as_wkt
100
+ as_ewkt(false,false,false)
101
+ end
102
+
103
+ #Outputs the geometry in georss format.
104
+ #Assumes the geometries are in latlon format, with x as lon and y as lat.
105
+ #Pass the <tt>:dialect</tt> option to swhit format. Possible values are: <tt>:simple</tt> (default), <tt>:w3cgeo</tt> and <tt>:gml</tt>.
106
+ def as_georss(options = {})
107
+ dialect= options[:dialect] || :simple
108
+ case(dialect)
109
+ when :simple
110
+ geom_attr = ""
111
+ geom_attr += " featuretypetag=\"#{options[:featuretypetag]}\"" if options[:featuretypetag]
112
+ geom_attr += " relationshiptag=\"#{options[:relationshiptag]}\"" if options[:relationshiptag]
113
+ geom_attr += " floor=\"#{options[:floor]}\"" if options[:floor]
114
+ geom_attr += " radius=\"#{options[:radius]}\"" if options[:radius]
115
+ geom_attr += " elev=\"#{options[:elev]}\"" if options[:elev]
116
+ georss_simple_representation(options.merge(:geom_attr => geom_attr))
117
+ when :w3cgeo
118
+ georss_w3cgeo_representation(options)
119
+ when :gml
120
+ georss_gml_representation(options)
121
+ end
122
+ end
123
+
124
+ #Iutputs the geometry in kml format : options are <tt>:id</tt>, <tt>:tesselate</tt>, <tt>:extrude</tt>,
125
+ #<tt>:altitude_mode</tt>. If the altitude_mode option is not present, the Z (if present) will not be output (since
126
+ #it won't be used by GE anyway: clampToGround is the default)
127
+ def as_kml(options = {})
128
+ id_attr = ""
129
+ id_attr = " id=\"#{options[:id]}\"" if options[:id]
130
+
131
+ geom_data = ""
132
+ geom_data += "<extrude>#{options[:extrude]}</extrude>\n" if options[:extrude]
133
+ geom_data += "<tesselate>#{options[:tesselate]}</tesselate>\n" if options[:tesselate]
134
+ geom_data += "<altitudeMode>#{options[:altitude_mode]}</altitudeMode>\n" if options[:altitude_mode]
135
+
136
+ allow_z = (with_z || !options[:altitude].nil? )&& (!options[:altitude_mode].nil?) && options[:atitude_mode] != "clampToGround"
137
+ fixed_z = options[:altitude]
138
+
139
+ kml_representation(options.merge(:id_attr => id_attr, :geom_data => geom_data, :allow_z => allow_z, :fixed_z => fixed_z))
140
+ end
141
+
142
+ #Creates a geometry based on a EWKB string. The actual class returned depends of the content of the string passed as argument. Since WKB strings are a subset of EWKB, they are also valid.
143
+ def self.from_ewkb(ewkb)
144
+ factory = GeometryFactory::new
145
+ ewkb_parser= EWKBParser::new(factory)
146
+ ewkb_parser.parse(ewkb)
147
+ factory.geometry
148
+ end
149
+ #Creates a geometry based on a HexEWKB string
150
+ def self.from_hex_ewkb(hexewkb)
151
+ factory = GeometryFactory::new
152
+ hexewkb_parser= HexEWKBParser::new(factory)
153
+ hexewkb_parser.parse(hexewkb)
154
+ factory.geometry
155
+ end
156
+ #Creates a geometry based on a EWKT string. Since WKT strings are a subset of EWKT, they are also valid.
157
+ def self.from_ewkt(ewkt)
158
+ factory = GeometryFactory::new
159
+ ewkt_parser= EWKTParser::new(factory)
160
+ ewkt_parser.parse(ewkt)
161
+ factory.geometry
162
+ end
163
+
164
+ #sends back a geometry based on the GeoRSS string passed as argument
165
+ def self.from_georss(georss)
166
+ georss_parser= GeorssParser::new
167
+ georss_parser.parse(georss)
168
+ georss_parser.geometry
169
+ end
170
+ #sends back an array: The first element is the goemetry based on the GeoRSS string passed as argument. The second one is the GeoRSSTags (found only with the Simple format)
171
+ def self.from_georss_with_tags(georss)
172
+ georss_parser= GeorssParser::new
173
+ georss_parser.parse(georss,true)
174
+ [georss_parser.geometry, georss_parser.georss_tags]
175
+ end
176
+
177
+ #Sends back a geometry from a KML encoded geometry string.
178
+ #Limitations : Only supports points, linestrings and polygons (no collection for now).
179
+ #Addapted from Pramukta's code
180
+ def self.from_kml(kml)
181
+ return GeoRuby::SimpleFeatures::Geometry.from_ewkt(kml_to_wkt(kml))
182
+ end
183
+
184
+ require 'rexml/document'
185
+ def self.kml_to_wkt(kml)
186
+ doc = REXML::Document.new(kml)
187
+ wkt = ""
188
+ if ["Point", "LineString", "Polygon" ].include?(doc.root.name)
189
+ case doc.root.name
190
+ when "Point" then
191
+ coords = doc.elements["/Point/coordinates"].text.gsub(/\n/," ")
192
+ wkt = doc.root.name.upcase + "(" + split_coords(coords).join(' ') + ")"
193
+ when "LineString" then
194
+ coords = doc.elements["/LineString/coordinates"].text.gsub(/\n/," ")
195
+ coords = split_coords(coords)
196
+ wkt = doc.root.name.upcase + "(" + coords.join(",") + ")"
197
+ when "Polygon" then
198
+ # polygons have one outer ring and zero or more inner rings
199
+ bounds = []
200
+ bounds << doc.elements["/Polygon/outerBoundaryIs/LinearRing/coordinates"].text
201
+ inner_coords_elements = doc.elements.each("/Polygon/innerBoundaryIs/LinearRing/coordinates") do |inner_coords|
202
+ inner_coords = inner_coords.text
203
+ bounds << inner_coords
204
+ end
205
+
206
+ wkt = doc.root.name.upcase + "(" + bounds.map do |bound|
207
+ bound.gsub!(/\n/, " ")
208
+ bound = split_coords(bound)
209
+ if bound.first != bound.last
210
+ bound.push bound.first
211
+ end
212
+ "(" + bound.join(",") + ")"
213
+ end.join(",") + ")"
214
+ end
215
+ end
216
+ return wkt
217
+ end
218
+
219
+ # Some GeoJSON files do not include srid info, so
220
+ # we provide an optional parameter
221
+ def self.from_geojson(geojson, srid=DEFAULT_SRID)
222
+ geojson_parser= GeojsonParser::new
223
+ geojson_parser.parse(geojson, srid)
224
+ geojson_parser.geometry
225
+ end
226
+
227
+ private
228
+
229
+ def self.split_coords(coords)
230
+ coords.split(" ").collect { |coord|
231
+ coord.gsub(","," ")
232
+ }
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,144 @@
1
+ require 'geo_ruby/simple_features/geometry'
2
+
3
+ module GeoRuby
4
+ module SimpleFeatures
5
+ #Represents a collection of arbitrary geometries
6
+ class GeometryCollection < Geometry
7
+ attr_reader :geometries
8
+
9
+ def initialize(srid = DEFAULT_SRID,with_z=false,with_m=false)
10
+ super(srid,with_z,with_m)
11
+ @geometries = []
12
+ end
13
+
14
+ #Delegate the unknown methods to the geometries array
15
+ def method_missing(method_name,*args,&b)
16
+ @geometries.send(method_name,*args,&b)
17
+ end
18
+
19
+ #Bounding box in 2D/3D. Returns an array of 2 points
20
+ def bounding_box
21
+ max_x, min_x, max_y, min_y = -Float::MAX, Float::MAX, -Float::MAX, Float::MAX, -Float::MAX, Float::MAX
22
+ if with_z
23
+ max_z, min_z = -Float::MAX, Float::MAX
24
+ each do |geometry|
25
+ bbox = geometry.bounding_box
26
+ sw = bbox[0]
27
+ ne = bbox[1]
28
+
29
+ max_y = ne.y if ne.y > max_y
30
+ min_y = sw.y if sw.y < min_y
31
+ max_x = ne.x if ne.x > max_x
32
+ min_x = sw.x if sw.x < min_x
33
+ max_z = ne.z if ne.z > max_z
34
+ min_z = sw.z if sw.z < min_z
35
+ end
36
+ [Point.from_x_y_z(min_x,min_y,min_z),Point.from_x_y_z(max_x,max_y,max_z)]
37
+ else
38
+ each do |geometry|
39
+ bbox = geometry.bounding_box
40
+ sw = bbox[0]
41
+ ne = bbox[1]
42
+
43
+ max_y = ne.y if ne.y > max_y
44
+ min_y = sw.y if sw.y < min_y
45
+ max_x = ne.x if ne.x > max_x
46
+ min_x = sw.x if sw.x < min_x
47
+ end
48
+ [Point.from_x_y(min_x,min_y),Point.from_x_y(max_x,max_y)]
49
+ end
50
+ end
51
+
52
+ def m_range
53
+ if with_m
54
+ max_m, min_m = -Float::MAX, Float::MAX
55
+ each do |lr|
56
+ lrmr = lr.m_range
57
+ max_m = lrmr[1] if lrmr[1] > max_m
58
+ min_m = lrmr[0] if lrmr[0] < min_m
59
+ end
60
+ [min_m,max_m]
61
+ else
62
+ [0,0]
63
+ end
64
+ end
65
+
66
+ #tests the equality of geometry collections
67
+ def ==(other_collection)
68
+ if(other_collection.class != self.class)
69
+ false
70
+ elsif length != other_collection.length
71
+ false
72
+ else
73
+ index=0
74
+ while index<length
75
+ return false if self[index] != other_collection[index]
76
+ index+=1
77
+ end
78
+ true
79
+ end
80
+ end
81
+
82
+ #Binary representation of the collection
83
+ def binary_representation(allow_z=true,allow_m=true) #:nodoc:
84
+ rep = [length].pack("V")
85
+ #output the list of geometries without outputting the SRID first and with the same setting regarding Z and M
86
+ each {|geometry| rep << geometry.as_ewkb(false,allow_z,allow_m) }
87
+ rep
88
+ end
89
+
90
+ #WKB geometry type of the collection
91
+ def binary_geometry_type #:nodoc:
92
+ 7
93
+ end
94
+
95
+ #Text representation of a geometry collection
96
+ def text_representation(allow_z=true,allow_m=true) #:nodoc:
97
+ @geometries.collect{|geometry| geometry.as_ewkt(false,allow_z,allow_m)}.join(",")
98
+ end
99
+
100
+ #WKT geometry type
101
+ def text_geometry_type #:nodoc:
102
+ "GEOMETRYCOLLECTION"
103
+ end
104
+
105
+ # simple geojson representation
106
+ # TODO add CRS / SRID support?
107
+ def to_json(options = {})
108
+ {:type => 'GeometryCollection',
109
+ :geometries => self.geometries}.to_json(options)
110
+ end
111
+ alias :as_geojson :to_json
112
+
113
+ #georss simple representation : outputs only the first geometry of the collection
114
+ def georss_simple_representation(options)#:nodoc:
115
+ self[0].georss_simple_representation(options)
116
+ end
117
+ #georss w3c representation : outputs the first point of the outer ring
118
+ def georss_w3cgeo_representation(options)#:nodoc:
119
+ self[0].georss_w3cgeo_representation(options)
120
+ end
121
+ #georss gml representation : outputs only the first geometry of the collection
122
+ def georss_gml_representation(options)#:nodoc:
123
+ self[0].georss_gml_representation(options)
124
+ end
125
+
126
+ #outputs the geometry in kml format
127
+ def kml_representation(options = {}) #:nodoc:
128
+ result = "<MultiGeometry#{options[:id_attr]}>\n"
129
+ options[:id_attr] = "" #the subgeometries do not have an ID
130
+ each do |geometry|
131
+ result += geometry.kml_representation(options)
132
+ end
133
+ result += "</MultiGeometry>\n"
134
+ end
135
+
136
+ #creates a new GeometryCollection from an array of geometries
137
+ def self.from_geometries(geometries,srid=DEFAULT_SRID,with_z=false,with_m=false)
138
+ geometry_collection = new(srid,with_z,with_m)
139
+ geometry_collection.concat(geometries)
140
+ geometry_collection
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,81 @@
1
+ require 'geo_ruby/simple_features/point'
2
+ require 'geo_ruby/simple_features/line_string'
3
+ require 'geo_ruby/simple_features/linear_ring'
4
+ require 'geo_ruby/simple_features/polygon'
5
+ require 'geo_ruby/simple_features/multi_point'
6
+ require 'geo_ruby/simple_features/multi_line_string'
7
+ require 'geo_ruby/simple_features/multi_polygon'
8
+ require 'geo_ruby/simple_features/geometry_collection'
9
+
10
+
11
+ module GeoRuby
12
+ module SimpleFeatures
13
+ #Creates a new geometry according to constructions received from a parser, for example EWKBParser.
14
+ class GeometryFactory
15
+ #the built geometry
16
+ attr_reader :geometry
17
+
18
+ def initialize
19
+ @geometry = nil
20
+ @geometry_stack = []
21
+ end
22
+ #resets the factory
23
+ def reset
24
+ @geometry = nil
25
+ @geometry_stack = []
26
+ end
27
+ #add a 2D point to the current geometry
28
+ def add_point_x_y(x,y)
29
+ @geometry_stack.last.set_x_y(x,y)
30
+ end
31
+ #add 2D points to the current geometry
32
+ def add_points_x_y(xy)
33
+ xy.each_slice(2) {|slice| add_point_x_y(*slice)}
34
+ end
35
+ #add a 3D point to the current geometry
36
+ def add_point_x_y_z(x,y,z)
37
+ @geometry_stack.last.set_x_y_z(x,y,z)
38
+ end
39
+ #add 3D points to the current geometry
40
+ def add_points_x_y_z(xyz)
41
+ xyz.each_slice(3) {|slice| add_point_x_y_z(*slice)}
42
+ end
43
+ #add a 2D point with M to the current geometry
44
+ def add_point_x_y_m(x,y,m)
45
+ @geometry_stack.last.set_x_y(x,y)
46
+ @geometry_stack.last.m=m
47
+ end
48
+ #add 2D points with M to the current geometry
49
+ def add_points_x_y_m(xym)
50
+ xym.each_slice(3) {|slice| add_point_x_y_m(*slice)}
51
+ end
52
+ #add a 3D point with M to the current geometry
53
+ def add_point_x_y_z_m(x,y,z,m)
54
+ @geometry_stack.last.set_x_y_z(x,y,z)
55
+ @geometry_stack.last.m=m
56
+ end
57
+ #add 3D points with M to the current geometry
58
+ def add_points_x_y_z_m(xyzm)
59
+ xyzm.each_slice(4) {|slice| add_point_x_y_z_m(*slice)}
60
+ end
61
+ #begin a geometry of type +geometry_type+
62
+ def begin_geometry(geometry_type,srid=DEFAULT_SRID)
63
+ geometry= geometry_type::new(srid)
64
+ @geometry= geometry if @geometry.nil?
65
+ @geometry_stack << geometry
66
+ end
67
+ #terminates the current geometry
68
+ def end_geometry(with_z=false,with_m=false)
69
+ @geometry=@geometry_stack.pop
70
+ @geometry.with_z=with_z
71
+ @geometry.with_m=with_m
72
+ #add the newly defined geometry to its parent if there is one
73
+ @geometry_stack.last << geometry if !@geometry_stack.empty?
74
+ end
75
+ #abort a geometry
76
+ def abort_geometry
77
+ reset
78
+ end
79
+ end
80
+ end
81
+ end