rgeo 0.1.16 → 0.1.17

Sign up to get free protection for your applications and to get access to all the features.
data/History.rdoc CHANGED
@@ -1,3 +1,10 @@
1
+ === 0.1.17 / 2010-11-20
2
+
3
+ * Implemented ActiveRecord adapters that cover MySQL Spatial for the mysql and mysql2 gems. SpatiaLite and PostGIS adapters are coming later.
4
+ * Added and documented FactoryGenerator.
5
+ * API CHANGE: WKRep parsers now take FactoryGenerator rather than the ad-hoc factory_from_srid.
6
+ * API CHANGE: Factory#override_cast now takes its optional flags in a hash so it can be extended more cleanly in the future.
7
+
1
8
  === 0.1.16 / 2010-11-18
2
9
 
3
10
  * Test coverage for WKB generator and parser; fixed a few bugs.
data/README.rdoc CHANGED
@@ -29,10 +29,11 @@ Use RGeo to:
29
29
  intersections, creating buffers, and computing lengths and areas.
30
30
  * Correctly handle spherical geometry, and compute projections for
31
31
  geographic data analysis.
32
- * Store and retrieve spatial data in industry standard spatial storage
32
+ * Store and retrieve spatial data in industry standard spatial database
33
33
  systems such as PostGIS.
34
34
  * Generate and interpret GeoJSON data for communication with common
35
- location-based services.
35
+ location-based services; and read and write ESRI shapefiles for
36
+ interaction with legacy GIS data.
36
37
  * Extend Ruby On Rails to handle location data in a web application.
37
38
  * Write spatial applications following the latest open standards from
38
39
  the Open Geospatial Consortium.
@@ -46,7 +47,7 @@ RGeo has the following prerequisites:
46
47
  * GEOS 3.2 or later highly recommended. Some functions will not be
47
48
  available without it. This C/C++ library may be available via your
48
49
  operating system's package manager, or you can download it from
49
- http://trac.osgeo.org/geos/
50
+ http://geos.osgeo.org/
50
51
 
51
52
  === Installation
52
53
 
@@ -63,23 +64,26 @@ library in the following locations:
63
64
  * /usr
64
65
 
65
66
  If GEOS has been installed in a different location, you must provide its
66
- installation prefix directory using the "--with-geos-dir" option.
67
- For example:
67
+ installation prefix directory using the "--with-geos-dir" option. This
68
+ option must be preceded by "--" to separate it, as a build switch, from
69
+ the switches interpreted by the gem command. For example:
68
70
 
69
- gem install rgeo -- --with-geos-dir=/var/local
71
+ gem install rgeo -- --with-geos-dir=/path/to/my/geos/installation
70
72
 
71
73
  === Known issues and to-do items
72
74
 
73
75
  RGeo is currently under development and several planned features are not
74
76
  yet complete. These include:
75
77
 
76
- * Some operations on SimpleCartesian and SimpleSpherical.
77
- * Rails (ActiveRecord or ActiveModel) integration.
78
- * Other third-party integration, including possibly SimpleGeo.
78
+ * SpatiaLite and PostGIS adapters for ActiveRecord.
79
79
  * Support for bbox and crs elements of GeoJSON.
80
- * Support for additional formats such as ESRI shapefiles.
80
+ * Support for ESRI shapefile reading and writing.
81
+ * Projection subsystem, and support for arbitrary projections in the geography module, utilizing proj4.
82
+ * Several more operations on SimpleCartesian and SimpleSpherical.
83
+ * Geography implementation utilizing ellipsoidal geometry, probably utilizing geographiclib.
84
+ * Additional third-party integration, possibly including SimpleGeo, GeoRSS, KML.
81
85
  * JRuby support via JTS integration.
82
- * Rubinius support for Geos integration.
86
+ * Rubinius support for GEOS integration.
83
87
 
84
88
  Additionally, not all implemented features are well-tested yet. In
85
89
  general, we currently consider this library to be "pre-alpha" quality,
@@ -102,6 +106,15 @@ RGeo is written by Daniel Azuma (http://www.daniel-azuma.com/).
102
106
 
103
107
  Development of RGeo is sponsored by GeoPage, Inc. (http://www.geopage.com/).
104
108
 
109
+ RGeo links with the Open Source Geospatial Foundation's GEOS library to
110
+ handle most Cartesian geometric calculations. GEOS and many other OSGeo
111
+ projects can be found on OSGeo's web site (http://www.osgeo.org/).
112
+
113
+ The ActiveRecord adapters owe some debt to the spatial_adapter plugin
114
+ (http://github.com/fragility/spatial_adapter). We made a few different
115
+ design decisions for RGeo, but studying the spatial_adapter source gave us
116
+ a head start on our implementation.
117
+
105
118
  === License
106
119
 
107
120
  Copyright 2010 Daniel Azuma
data/Version CHANGED
@@ -1 +1 @@
1
- 0.1.16
1
+ 0.1.17
@@ -0,0 +1,124 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # MysqlSpatial adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'rgeo/active_record/mysql_common'
38
+ require 'active_record/connection_adapters/mysql2_adapter'
39
+
40
+
41
+ module ActiveRecord # :nodoc:
42
+
43
+ class Base # :nodoc:
44
+
45
+
46
+ def self.mysql2spatial_connection(config_) # :nodoc:
47
+ config_[:username] = 'root' if config_[:username].nil?
48
+ if ::Mysql2::Client.const_defined?(:FOUND_ROWS)
49
+ config_[:flags] = ::Mysql2::Client::FOUND_ROWS
50
+ end
51
+ client_ = ::Mysql2::Client.new(config_.symbolize_keys)
52
+ options_ = [config_[:host], config_[:username], config_[:password], config_[:database], config_[:port], config_[:socket], 0]
53
+ ConnectionAdapters::Mysql2SpatialAdapter.new(client_, logger, options_, config_)
54
+ end
55
+
56
+
57
+ end
58
+
59
+
60
+ module ConnectionAdapters
61
+
62
+ class Mysql2SpatialAdapter < Mysql2Adapter
63
+
64
+
65
+ class SpatialColumn < ConnectionAdapters::Mysql2Column
66
+
67
+ include ::RGeo::ActiveRecord::MysqlCommon::ColumnMethods
68
+
69
+ end
70
+
71
+
72
+ include ::RGeo::ActiveRecord::MysqlCommon::AdapterMethods
73
+
74
+
75
+ ADAPTER_NAME = 'Mysql2Spatial'.freeze
76
+
77
+ NATIVE_DATABASE_TYPES = Mysql2Adapter::NATIVE_DATABASE_TYPES.merge(:geometry => {:name => "geometry"}, :point => {:name => "point"}, :line_string => {:name => "linestring"}, :polygon => {:name => "polygon"}, :geometry_collection => {:name => "geometrycollection"}, :multi_point => {:name => "multipoint"}, :multi_line_string => {:name => "multilinestring"}, :multi_polygon => {:name => "multipolygon"})
78
+
79
+
80
+ def native_database_types
81
+ NATIVE_DATABASE_TYPES
82
+ end
83
+
84
+
85
+ def adapter_name
86
+ ADAPTER_NAME
87
+ end
88
+
89
+
90
+ def columns(table_name_, name_=nil)
91
+ result_ = execute("SHOW FIELDS FROM #{quote_table_name(table_name_)}", :skip_logging)
92
+ columns_ = []
93
+ result_.each(:symbolize_keys => true, :as => :hash) do |field_|
94
+ columns_ << SpatialColumn.new(field_[:Field], field_[:Default], field_[:Type], field_[:Null] == "YES")
95
+ end
96
+ columns_
97
+ end
98
+
99
+
100
+ def indexes(table_name_, name_=nil)
101
+ indexes_ = []
102
+ current_index_ = nil
103
+ result_ = execute("SHOW KEYS FROM #{quote_table_name(table_name_)}", name_)
104
+ result_.each(:symbolize_keys => true, :as => :hash) do |row_|
105
+ if current_index_ != row_[:Key_name]
106
+ next if row_[:Key_name] == 'PRIMARY' # skip the primary key
107
+ current_index_ = row_[:Key_name]
108
+ new_index_ = ::RGeo::ActiveRecord::MysqlCommon::IndexDefinition.new(row_[:Table], row_[:Key_name], row_[:Non_unique] == 0, [], [])
109
+ new_index_.spatial = row_[:Index_type] == 'SPATIAL'
110
+ indexes_ << new_index_
111
+ end
112
+ indexes_.last.columns << row_[:Column_name]
113
+ indexes_.last.lengths << row_[:Sub_part]
114
+ end
115
+ indexes_
116
+ end
117
+
118
+
119
+ end
120
+
121
+ end
122
+
123
+
124
+ end
@@ -0,0 +1,134 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # MysqlSpatial adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'rgeo/active_record/mysql_common'
38
+ require 'active_record/connection_adapters/mysql_adapter'
39
+
40
+
41
+ module ActiveRecord # :nodoc:
42
+
43
+ class Base # :nodoc:
44
+
45
+
46
+ def self.mysqlspatial_connection(config_) # :nodoc:
47
+ unless defined?(::Mysql)
48
+ begin
49
+ require 'mysql'
50
+ rescue ::LoadError
51
+ raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'"
52
+ end
53
+ unless defined?(::Mysql::Result) && ::Mysql::Result.method_defined?(:each_hash)
54
+ raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'"
55
+ end
56
+ end
57
+ config_ = config_.symbolize_keys
58
+ mysql_ = ::Mysql.init
59
+ mysql_.ssl_set(config_[:sslkey], config_[:sslcert], config_[:sslca], config_[:sslcapath], config_[:sslcipher]) if config_[:sslca] || config_[:sslkey]
60
+ default_flags_ = ::Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? ::Mysql::CLIENT_MULTI_RESULTS : 0
61
+ default_flags_ |= ::Mysql::CLIENT_FOUND_ROWS if ::Mysql.const_defined?(:CLIENT_FOUND_ROWS)
62
+ options_ = [config_[:host], config_[:username] ? config_[:username].to_s : 'root', config_[:password].to_s, config_[:database], config_[:port], config_[:socket], default_flags_]
63
+ ConnectionAdapters::MysqlSpatialAdapter.new(mysql_, logger, options_, config_)
64
+ end
65
+
66
+
67
+ end
68
+
69
+
70
+ module ConnectionAdapters
71
+
72
+ class MysqlSpatialAdapter < MysqlAdapter
73
+
74
+
75
+ class SpatialColumn < ConnectionAdapters::MysqlColumn
76
+
77
+ include ::RGeo::ActiveRecord::MysqlCommon::ColumnMethods
78
+
79
+ end
80
+
81
+
82
+ include ::RGeo::ActiveRecord::MysqlCommon::AdapterMethods
83
+
84
+
85
+ ADAPTER_NAME = 'MysqlSpatial'.freeze
86
+
87
+ NATIVE_DATABASE_TYPES = MysqlAdapter::NATIVE_DATABASE_TYPES.merge(:geometry => {:name => "geometry"}, :point => {:name => "point"}, :line_string => {:name => "linestring"}, :polygon => {:name => "polygon"}, :geometry_collection => {:name => "geometrycollection"}, :multi_point => {:name => "multipoint"}, :multi_line_string => {:name => "multilinestring"}, :multi_polygon => {:name => "multipolygon"})
88
+
89
+
90
+ def native_database_types
91
+ NATIVE_DATABASE_TYPES
92
+ end
93
+
94
+
95
+ def adapter_name
96
+ ADAPTER_NAME
97
+ end
98
+
99
+
100
+ def columns(table_name_, name_=nil)
101
+ result_ = execute("SHOW FIELDS FROM #{quote_table_name(table_name_)}", :skip_logging)
102
+ columns_ = []
103
+ result_.each{ |field_| columns_ << SpatialColumn.new(field_[0], field_[4], field_[1], field_[2] == "YES") }
104
+ result_.free
105
+ columns_
106
+ end
107
+
108
+
109
+ def indexes(table_name_, name_=nil)
110
+ indexes_ = []
111
+ current_index_ = nil
112
+ result_ = execute("SHOW KEYS FROM #{quote_table_name(table_name_)}", name_)
113
+ result_.each do |row_|
114
+ if current_index_ != row_[2]
115
+ next if row_[2] == "PRIMARY" # skip the primary key
116
+ current_index_ = row_[2]
117
+ new_index_ = ::RGeo::ActiveRecord::MysqlCommon::IndexDefinition.new(row_[0], row_[2], row_[1] == "0", [], [])
118
+ new_index_.spatial = row_[10] == 'SPATIAL'
119
+ indexes_ << new_index_
120
+ end
121
+ indexes_.last.columns << row_[4]
122
+ indexes_.last.lengths << row_[7]
123
+ end
124
+ result_.free
125
+ indexes_
126
+ end
127
+
128
+
129
+ end
130
+
131
+ end
132
+
133
+
134
+ end
@@ -0,0 +1,73 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Mysqlgeo adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'arel'
38
+
39
+
40
+ module Arel
41
+
42
+ module Attributes
43
+
44
+ class Geometry < Attribute; end
45
+
46
+ class << self
47
+ alias_method :for_without_geometry, :for
48
+ def for(column_)
49
+ column_.type == :geometry ? Geometry : for_without_geometry(column_)
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ module Visitors
56
+
57
+ class Dot
58
+ alias :visit_Arel_Attributes_Geometry :visit_Arel_Attribute
59
+ alias :visit_RGeo_Features_Geometry :visit_String
60
+ end
61
+
62
+ class ToSql
63
+ alias :visit_Arel_Attributes_Geometry :visit_Arel_Attributes_Attribute
64
+ alias :visit_RGeo_Features_Geometry :visit_String
65
+ end
66
+
67
+ VISITORS['postgis'] = ::Arel::Visitors::PostgreSQL
68
+ VISITORS['mysqlspatial'] = ::Arel::Visitors::MySQL
69
+ VISITORS['spatialite'] = ::Arel::Visitors::SQLite
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,72 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Mysqlgeo adapter for ActiveRecord
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2010 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ require 'active_record'
38
+
39
+
40
+ module ActiveRecord # :nodoc:
41
+
42
+ class Base # :nodoc:
43
+
44
+ self.attribute_types_cached_by_default << :geometry
45
+
46
+ class_attribute :rgeo_default_factory, :instance_writer => false
47
+ self.rgeo_default_factory = nil
48
+
49
+ class_attribute :rgeo_factory_generator, :instance_writer => false
50
+ self.rgeo_factory_generator = nil
51
+
52
+ def self.to_generate_rgeo_factory(&block_)
53
+ self.rgeo_factory_generator = block_
54
+ end
55
+
56
+ class << self
57
+
58
+ alias_method :columns_without_rgeo_modification, :columns
59
+ def columns
60
+ unless defined?(@columns) && @columns
61
+ columns_without_rgeo_modification.each do |column_|
62
+ column_.ar_class = self if column_.respond_to?(:ar_class=)
63
+ end
64
+ end
65
+ @columns
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end