geo_tools 1.0.1
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.
- data/MIT-LICENSE +20 -0
- data/README.md +108 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/air_blade/geo_tools/form_helpers.rb +166 -0
- data/lib/air_blade/geo_tools/location.rb +266 -0
- data/lib/air_blade/geo_tools/validations.rb +75 -0
- data/lib/geo_tools.rb +3 -0
- data/tasks/geo_tools_tasks.rake +4 -0
- data/test/geo_tools_test.rb +160 -0
- data/test/schema.rb +9 -0
- data/test/test_helper.rb +22 -0
- data/uninstall.rb +1 -0
- metadata +71 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# GeoTools
|
2
|
+
|
3
|
+
|
4
|
+
You have lots of plugin choices if you want to geocode North American addresses, or find all the locations near somewhere. But few help you with forms and validation.
|
5
|
+
|
6
|
+
This plugin does four things:
|
7
|
+
|
8
|
+
* Adds `latitude_field` and `longitude_field` form helpers to Rails' default form builder.
|
9
|
+
* Lets your model acts_as_location, to work seamlessly with the form helpers.
|
10
|
+
* Validates the location data entered on the form and in the database.
|
11
|
+
* Gives you a `within` named scope to find all lcoations within a given bounding box, such as you would have on a Google map.
|
12
|
+
|
13
|
+
|
14
|
+
## Assumptions
|
15
|
+
|
16
|
+
* Any model that acts_as_location has integers defined for each component of the latitude and longitude:
|
17
|
+
|
18
|
+
# In your model's migration's self.up method:
|
19
|
+
create_table :thingies do |t|
|
20
|
+
# Your model's various fields.
|
21
|
+
t.string :name
|
22
|
+
t.timestamps
|
23
|
+
...
|
24
|
+
|
25
|
+
# Stuff GeoTools needs:
|
26
|
+
t.integer :latitude_degrees, :latitude_minutes, :latitude_decimal_minutes, :latitude_decimal_minutes_width
|
27
|
+
t.string :latitude_hemisphere
|
28
|
+
t.integer :longitude_degrees, :longitude_minutes, :longitude_decimal_minutes, :longitude_decimal_minutes_width
|
29
|
+
t.string :longitude_hemisphere
|
30
|
+
end
|
31
|
+
|
32
|
+
Storing the components separately like this avoids the round-trip rounding errors you get when using floating point numbers. If you need a floating point representation in the database, for example to use a mapping plugin, simply add an after_update callback to your model to write the float value to the database.
|
33
|
+
|
34
|
+
* A latitude should be entered on a form like this:
|
35
|
+
|
36
|
+
xx <degree symbol> yy <decimal point> zz h
|
37
|
+
|
38
|
+
where:
|
39
|
+
|
40
|
+
xx is degrees (0 <= integer <= 90; maximum length of 2 digits)
|
41
|
+
yy is minutes (0 <= integer <= 59; maximum length of 2 digits; optional; defaults to 0)
|
42
|
+
zz is decimal-minutes (0 <= integer <= 99; maximum length of 2 digits; optional; defaults to 0)
|
43
|
+
h is hemisphere ('N' or 'S')
|
44
|
+
|
45
|
+
Note with decimal minutes 2, 20 and 200000 are equivalent. This is because 3.2, 3.20 and 3.200000 are equivalent.
|
46
|
+
|
47
|
+
* Similarly, a longitude should be entered on a form like this:
|
48
|
+
|
49
|
+
xxx <degree symbol> yy <decimal point> zz h
|
50
|
+
|
51
|
+
where:
|
52
|
+
|
53
|
+
xxx is degrees (0 <= integer <= 180; maximum length of 3 digits)
|
54
|
+
yy is minutes (0 <= integer <= 59; maximum length of 2 digits; optional; defaults to 0)
|
55
|
+
zz is decimal-minutes (0 <= integer <= 99; maximum length of 2 digits; optional; defaults to 0)
|
56
|
+
h is hemisphere ('E' or 'W')
|
57
|
+
|
58
|
+
|
59
|
+
## Example
|
60
|
+
|
61
|
+
# Model
|
62
|
+
class Treasure < ActiveRecord::Base
|
63
|
+
acts_as_location
|
64
|
+
end
|
65
|
+
|
66
|
+
# View
|
67
|
+
<% form_for @treasure do |f| %>
|
68
|
+
<%= f.text_field :spot_marked_by %>
|
69
|
+
<%= f.latitude_field :latitude %>
|
70
|
+
<%= f.longitude_field :longitude %>
|
71
|
+
<% end %>
|
72
|
+
|
73
|
+
# Controller
|
74
|
+
# ...same as usual...
|
75
|
+
|
76
|
+
You'll get validation on every field (degrees, minutes, decimal-minutes, hemisphere) generated by the form helpers, though not the overall value any more (TBD).
|
77
|
+
|
78
|
+
Here's an example script/console session:
|
79
|
+
|
80
|
+
>> puts Treasure.find(:first).location
|
81
|
+
12°34.56′N, 012°34.56′W # N.B. If this looks weird online, set your browser's text encoding to UTF-8.
|
82
|
+
|
83
|
+
>> puts Treasure.find(:first).location.latitude
|
84
|
+
12.576
|
85
|
+
|
86
|
+
>> puts Treasure.find(:first).location.longitude
|
87
|
+
-12.576
|
88
|
+
|
89
|
+
|
90
|
+
## To Do
|
91
|
+
|
92
|
+
* Get tests to run transactionally so we don't have to clean out database in every single #setup method.
|
93
|
+
* Add a validation for the overall latitude and longitude values (to catch for example 90°00.01′N).
|
94
|
+
* Use `method` in the form helpers so user can give database columns different names (e.g. my_lat_degrees, etc).
|
95
|
+
See the way Paperclip allows different attachment names.
|
96
|
+
* DRY up form helper methods.
|
97
|
+
* DRY up location.rb.
|
98
|
+
* Investigate implementing with ActiveRecord's multiparameter assignment.
|
99
|
+
|
100
|
+
|
101
|
+
## Feedback
|
102
|
+
|
103
|
+
Yes please! --> boss@airbladesoftware.com
|
104
|
+
|
105
|
+
|
106
|
+
## Intellectual Property
|
107
|
+
|
108
|
+
Copyright (c) 2010 Andy Stewart, AirBlade Software Ltd. Released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the geo_tools plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the geo_tools plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'GeoTools'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
require 'jeweler'
|
27
|
+
Jeweler::Tasks.new do |gemspec|
|
28
|
+
gemspec.name = 'geo_tools'
|
29
|
+
gemspec.summary = 'View helpers, validations, and named scopes for locations.'
|
30
|
+
gemspec.email = 'boss@airbladesoftware.com'
|
31
|
+
gemspec.homepage = 'http://github.com/airblade/geo_tools'
|
32
|
+
gemspec.authors = ['Andy Stewart']
|
33
|
+
end
|
34
|
+
Jeweler::GemcutterTasks.new
|
35
|
+
rescue LoadError
|
36
|
+
puts 'Jeweler not available. Install it with: gem install jeweler'
|
37
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.1
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'geo_tools'
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module AirBlade
|
2
|
+
module GeoTools
|
3
|
+
|
4
|
+
module FormHelpers
|
5
|
+
|
6
|
+
# Options:
|
7
|
+
# :latitude
|
8
|
+
# :degrees
|
9
|
+
# :symbol
|
10
|
+
# :minutes
|
11
|
+
# :symbol
|
12
|
+
# :decimal_minutes
|
13
|
+
# :symbol
|
14
|
+
# :maxlength
|
15
|
+
#
|
16
|
+
# Assumes the latitude field is called 'latitude'.
|
17
|
+
#
|
18
|
+
# The 'method' argument is for consistency with other field helpers. We don't use it
|
19
|
+
# when using the normal Rails form builder.
|
20
|
+
#
|
21
|
+
# 1/100th of a minute of latitude (or equitorial longitude) is approximately 20m.
|
22
|
+
def latitude_field(method, options = {})
|
23
|
+
opts = {
|
24
|
+
:degrees => { :symbol => '°' },
|
25
|
+
:minutes => { :symbol => '.' },
|
26
|
+
:decimal_minutes => { :symbol => '′', :maxlength => 2 },
|
27
|
+
}
|
28
|
+
lat_options = options.delete :latitude
|
29
|
+
opts.merge! lat_options if lat_options
|
30
|
+
|
31
|
+
output = []
|
32
|
+
|
33
|
+
# Degrees
|
34
|
+
width = 2
|
35
|
+
output << plain_text_field("latitude_degrees",
|
36
|
+
options.merge(:maxlength => width,
|
37
|
+
:value => "%0#{width}d" % @object.send("latitude_degrees")))
|
38
|
+
output << opts[:degrees][:symbol]
|
39
|
+
|
40
|
+
# Minutes
|
41
|
+
width = 2
|
42
|
+
output << plain_text_field("latitude_minutes",
|
43
|
+
options.merge(:maxlength => width,
|
44
|
+
:value => "%0#{width}d" % @object.send("latitude_minutes")))
|
45
|
+
output << opts[:minutes][:symbol]
|
46
|
+
|
47
|
+
# Decimal minutes
|
48
|
+
width = opts[:decimal_minutes][:maxlength]
|
49
|
+
output << plain_text_field("latitude_decimal_minutes",
|
50
|
+
options.merge(:maxlength => width,
|
51
|
+
:value => @object.send("latitude_decimal_minutes_as_string").ljust(width, '0')))
|
52
|
+
output << opts[:decimal_minutes][:symbol]
|
53
|
+
|
54
|
+
# Hemisphere.
|
55
|
+
# Hmm, we pass the options in the html_options position.
|
56
|
+
output << plain_select("latitude_hemisphere", %w( N S ), {}, options)
|
57
|
+
|
58
|
+
output.join "\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
def longitude_field(method, options = {})
|
62
|
+
opts = {
|
63
|
+
:degrees => { :symbol => '°' },
|
64
|
+
:minutes => { :symbol => '.' },
|
65
|
+
:decimal_minutes => { :symbol => '′', :maxlength => 2 },
|
66
|
+
}
|
67
|
+
long_options = options.delete :longitude
|
68
|
+
opts.merge! long_options if long_options
|
69
|
+
|
70
|
+
output = []
|
71
|
+
|
72
|
+
# Degrees
|
73
|
+
width = 3
|
74
|
+
output << plain_text_field("longitude_degrees",
|
75
|
+
options.merge(:maxlength => width,
|
76
|
+
:value => "%0#{width}d" % @object.send("longitude_degrees")))
|
77
|
+
output << opts[:degrees][:symbol]
|
78
|
+
|
79
|
+
# Minutes
|
80
|
+
width = 2
|
81
|
+
output << plain_text_field("longitude_minutes",
|
82
|
+
options.merge(:maxlength => width,
|
83
|
+
:value => "%0#{width}d" % @object.send("longitude_minutes")))
|
84
|
+
output << opts[:minutes][:symbol]
|
85
|
+
|
86
|
+
# Decimal minutes
|
87
|
+
width = opts[:decimal_minutes][:maxlength]
|
88
|
+
output << plain_text_field("longitude_decimal_minutes",
|
89
|
+
options.merge(:maxlength => width,
|
90
|
+
:value => @object.send("longitude_decimal_minutes_as_string").ljust(width, '0')))
|
91
|
+
output << opts[:decimal_minutes][:symbol]
|
92
|
+
|
93
|
+
# Hemisphere.
|
94
|
+
# Hmm, we pass the options in the html_options position.
|
95
|
+
output << plain_select("longitude_hemisphere", %w( E W ), {}, options)
|
96
|
+
|
97
|
+
output.join "\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
# A layer of indirection to allow us always to use a plain field helpers,
|
101
|
+
# regardless of the form builder being used.
|
102
|
+
|
103
|
+
def plain_text_field(*a, &b)
|
104
|
+
text_field(*a, &b)
|
105
|
+
end
|
106
|
+
|
107
|
+
def plain_select(*a, &b)
|
108
|
+
select(*a, &b)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
module AirBuddFormHelpers
|
114
|
+
include AirBlade::GeoTools::FormHelpers
|
115
|
+
alias_method :plain_latitude_field, :latitude_field
|
116
|
+
alias_method :plain_longitude_field, :longitude_field
|
117
|
+
|
118
|
+
# Override latitude_field to wrap it with the custom form builder gubbins.
|
119
|
+
# http://github.com/airblade/air_budd_form_builder/tree/master/lib/air_blade/air_budd/form_builder.rb
|
120
|
+
def latitude_field(method, options = {}, html_options = {})
|
121
|
+
@template.content_tag('p',
|
122
|
+
label_element(method, options, html_options) +
|
123
|
+
(
|
124
|
+
plain_latitude_field method, options
|
125
|
+
) +
|
126
|
+
hint_element(options),
|
127
|
+
(errors_for?(method) ? {:class => 'error'} : {})
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Override longitude_field to wrap it with the custom form builder gubbins.
|
132
|
+
# http://github.com/airblade/air_budd_form_builder/tree/master/lib/air_blade/air_budd/form_builder.rb
|
133
|
+
def longitude_field(method, options = {}, html_options = {})
|
134
|
+
@template.content_tag('p',
|
135
|
+
label_element(method, options, html_options) +
|
136
|
+
(
|
137
|
+
plain_longitude_field method, options
|
138
|
+
) +
|
139
|
+
hint_element(options),
|
140
|
+
(errors_for?(method) ? {:class => 'error'} : {})
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Use the standard Rails helpers for text fields and selects.
|
145
|
+
# These are overridden by the AirBudd form builder, so we define
|
146
|
+
# them ourselves.
|
147
|
+
|
148
|
+
def plain_text_field(method, options = {})
|
149
|
+
# From ActionView::Helpers::FormBuilder
|
150
|
+
@template.send('text_field', @object_name, method, objectify_options(options))
|
151
|
+
end
|
152
|
+
def plain_select(method, choices, options = {}, html_options = {})
|
153
|
+
# From ActionView::Helpers::FormOptionsHelper::FormBuilder
|
154
|
+
@template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
# Integrate with standard Rails form builder.
|
163
|
+
ActionView::Helpers::FormBuilder.send :include, AirBlade::GeoTools::FormHelpers
|
164
|
+
|
165
|
+
# Integrate with custom AirBudd form builder.
|
166
|
+
AirBlade::AirBudd::FormBuilder.send(:include, AirBlade::GeoTools::AirBuddFormHelpers) rescue nil
|
@@ -0,0 +1,266 @@
|
|
1
|
+
module AirBlade
|
2
|
+
module GeoTools
|
3
|
+
module Location
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
# Lazy loading pattern.
|
7
|
+
base.extend ActMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ActMethods
|
11
|
+
def acts_as_location
|
12
|
+
unless included_modules.include? InstanceMethods
|
13
|
+
extend ClassMethods
|
14
|
+
include InstanceMethods
|
15
|
+
|
16
|
+
code = <<-END
|
17
|
+
validates_numericality_of_for :latitude_degrees,
|
18
|
+
:only_integer => true,
|
19
|
+
:greater_than_or_equal_to => 0,
|
20
|
+
:less_than_or_equal_to => 90,
|
21
|
+
:message => 'Degrees are invalid',
|
22
|
+
:for => :latitude
|
23
|
+
|
24
|
+
validates_numericality_of_for :latitude_minutes,
|
25
|
+
:only_integer => true,
|
26
|
+
:greater_than_or_equal_to => 0,
|
27
|
+
:less_than => 60,
|
28
|
+
:message => 'Minutes are invalid',
|
29
|
+
:for => :latitude
|
30
|
+
|
31
|
+
validates_numericality_of_for :latitude_decimal_minutes,
|
32
|
+
:only_integer => true,
|
33
|
+
:greater_than_or_equal_to => 0,
|
34
|
+
:message => 'Decimal minutes are invalid',
|
35
|
+
:for => :latitude
|
36
|
+
|
37
|
+
validates_numericality_of_for :latitude_decimal_minutes_width,
|
38
|
+
:only_integer => true,
|
39
|
+
:greater_than_or_equal_to => 0,
|
40
|
+
:for => :latitude
|
41
|
+
|
42
|
+
validates_inclusion_of_for :latitude_hemisphere,
|
43
|
+
:in => %w( N S ),
|
44
|
+
:message => 'Hemisphere is invalid',
|
45
|
+
:for => :latitude
|
46
|
+
|
47
|
+
validates_numericality_of_for :longitude_degrees,
|
48
|
+
:only_integer => true,
|
49
|
+
:greater_than_or_equal_to => 0,
|
50
|
+
:less_than_or_equal_to => 180,
|
51
|
+
:message => 'Degrees are invalid',
|
52
|
+
:for => :longitude
|
53
|
+
|
54
|
+
validates_numericality_of_for :longitude_minutes,
|
55
|
+
:only_integer => true,
|
56
|
+
:greater_than_or_equal_to => 0,
|
57
|
+
:less_than => 60,
|
58
|
+
:message => 'Minutes are invalid',
|
59
|
+
:for => :longitude
|
60
|
+
|
61
|
+
validates_numericality_of_for :longitude_decimal_minutes,
|
62
|
+
:only_integer => true,
|
63
|
+
:greater_than_or_equal_to => 0,
|
64
|
+
:message => 'Decimal minutes are invalid',
|
65
|
+
:for => :longitude
|
66
|
+
|
67
|
+
validates_numericality_of_for :longitude_decimal_minutes_width,
|
68
|
+
:only_integer => true,
|
69
|
+
:greater_than_or_equal_to => 0,
|
70
|
+
:for => :longitude
|
71
|
+
|
72
|
+
validates_inclusion_of_for :longitude_hemisphere,
|
73
|
+
:in => %w( E W ),
|
74
|
+
:message => 'Hemisphere is invalid',
|
75
|
+
:for => :longitude
|
76
|
+
|
77
|
+
before_validation :set_empty_values
|
78
|
+
END
|
79
|
+
class_eval code, __FILE__, __LINE__
|
80
|
+
|
81
|
+
# Returns all locations within the given bounding box, to an accuracy of 1 minute.
|
82
|
+
#
|
83
|
+
# This is useful for finding all locations within the area covered by a Google map.
|
84
|
+
#
|
85
|
+
# The parameters should be positive/negative floats.
|
86
|
+
named_scope :within, lambda { |sw_lat, sw_lng, ne_lat, ne_lng|
|
87
|
+
sw_lat_degs = sw_lat.to_i.abs
|
88
|
+
sw_lat_mins = ((sw_lat - sw_lat.to_i) * 60.0).round.abs
|
89
|
+
ne_lat_degs = ne_lat.to_i.abs
|
90
|
+
ne_lat_mins = ((ne_lat - ne_lat.to_i) * 60.0).round.abs
|
91
|
+
|
92
|
+
sw_lng_degs = sw_lng.to_i.abs
|
93
|
+
sw_lng_mins = ((sw_lng - sw_lng.to_i) * 60.0).round.abs
|
94
|
+
ne_lng_degs = ne_lng.to_i.abs
|
95
|
+
ne_lng_mins = ((ne_lng - ne_lng.to_i) * 60.0).round.abs
|
96
|
+
|
97
|
+
# Latitude conditions.
|
98
|
+
if sw_lat > 0 && ne_lat > 0 # northern hemisphere
|
99
|
+
condition_lat_h = 'latitude_hemisphere = "N"'
|
100
|
+
condition_lat_sw = ["(latitude_degrees > ?) OR (latitude_degrees = ? AND latitude_minutes >= ?)", sw_lat_degs, sw_lat_degs, sw_lat_mins]
|
101
|
+
condition_lat_ne = ["(latitude_degrees < ?) OR (latitude_degrees = ? AND latitude_minutes <= ?)", ne_lat_degs, ne_lat_degs, ne_lat_mins]
|
102
|
+
condition_lat = merge_conditions condition_lat_h, condition_lat_sw, condition_lat_ne
|
103
|
+
|
104
|
+
elsif sw_lat < 0 && ne_lat < 0 # southern hemisphere
|
105
|
+
condition_lat_h = 'latitude_hemisphere = "S"'
|
106
|
+
condition_lat_sw = ["(latitude_degrees < ?) OR (latitude_degrees = ? AND latitude_minutes <= ?)", sw_lat_degs, sw_lat_degs, sw_lat_mins]
|
107
|
+
condition_lat_ne = ["(latitude_degrees > ?) OR (latitude_degrees = ? AND latitude_minutes >= ?)", ne_lat_degs, ne_lat_degs, ne_lat_mins]
|
108
|
+
condition_lat = merge_conditions condition_lat_h, condition_lat_sw, condition_lat_ne
|
109
|
+
|
110
|
+
elsif sw_lat <= 0 && ne_lat >= 0 # straddles equator
|
111
|
+
condition_lat_h = 'latitude_hemisphere = "S"'
|
112
|
+
condition_lat_sw = ["(latitude_degrees < ?) OR (latitude_degrees = ? AND latitude_minutes <= ?)", sw_lat_degs, sw_lat_degs, sw_lat_mins]
|
113
|
+
condition_lat_s = merge_conditions condition_lat_h, condition_lat_sw
|
114
|
+
|
115
|
+
condition_lat_h = 'latitude_hemisphere = "N"'
|
116
|
+
condition_lat_ne = ["(latitude_degrees < ?) OR (latitude_degrees = ? AND latitude_minutes <= ?)", ne_lat_degs, ne_lat_degs, ne_lat_mins]
|
117
|
+
condition_lat_n = merge_conditions condition_lat_h, condition_lat_ne
|
118
|
+
|
119
|
+
condition_lat = merge_or_conditions condition_lat_s, condition_lat_n
|
120
|
+
end
|
121
|
+
|
122
|
+
# Longitude conditions.
|
123
|
+
if sw_lng > 0 && ne_lng > 0 # eastern hemisphere
|
124
|
+
condition_lng_h = 'longitude_hemisphere = "E"'
|
125
|
+
condition_lng_sw = ["(longitude_degrees > ?) OR (longitude_degrees = ? AND longitude_minutes >= ?)", sw_lng_degs, sw_lng_degs, sw_lng_mins]
|
126
|
+
condition_lng_ne = ["(longitude_degrees < ?) OR (longitude_degrees = ? AND longitude_minutes <= ?)", ne_lng_degs, ne_lng_degs, ne_lng_mins]
|
127
|
+
condition_lng = merge_conditions condition_lng_h, condition_lng_sw, condition_lng_ne
|
128
|
+
|
129
|
+
elsif sw_lng < 0 && ne_lng < 0 # western hemisphere
|
130
|
+
condition_lng_h = 'longitude_hemisphere = "W"'
|
131
|
+
condition_lng_sw = ["(longitude_degrees < ?) OR (longitude_degrees = ? AND longitude_minutes <= ?)", sw_lng_degs, sw_lng_degs, sw_lng_mins]
|
132
|
+
condition_lng_ne = ["(longitude_degrees > ?) OR (longitude_degrees = ? AND longitude_minutes >= ?)", ne_lng_degs, ne_lng_degs, ne_lng_mins]
|
133
|
+
condition_lng = merge_conditions condition_lng_h, condition_lng_sw, condition_lng_ne
|
134
|
+
|
135
|
+
elsif sw_lng <= 0 && ne_lng >= 0 # straddles prime meridian
|
136
|
+
condition_lng_h = 'longitude_hemisphere = "W"'
|
137
|
+
condition_lng_sw = ["(longitude_degrees < ?) OR (longitude_degrees = ? AND longitude_minutes <= ?)", sw_lng_degs, sw_lng_degs, sw_lng_mins]
|
138
|
+
condition_lng_w = merge_conditions condition_lng_h, condition_lng_sw
|
139
|
+
|
140
|
+
condition_lng_h = 'longitude_hemisphere = "E"'
|
141
|
+
condition_lng_ne = ["(longitude_degrees < ?) OR (longitude_degrees = ? AND longitude_minutes <= ?)", ne_lng_degs, ne_lng_degs, ne_lng_mins]
|
142
|
+
condition_lng_e = merge_conditions condition_lng_h, condition_lng_ne
|
143
|
+
|
144
|
+
condition_lng = merge_or_conditions condition_lng_w, condition_lng_e
|
145
|
+
end
|
146
|
+
|
147
|
+
# Combined latitude and longitude conditions.
|
148
|
+
{:conditions => merge_conditions(condition_lat, condition_lng)}
|
149
|
+
}
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module ClassMethods
|
156
|
+
# Merges conditions so that the result is a valid +condition+.
|
157
|
+
# Adapted from ActiveRecord::Base#merge_conditions.
|
158
|
+
def merge_or_conditions(*conditions)
|
159
|
+
segments = []
|
160
|
+
|
161
|
+
conditions.each do |condition|
|
162
|
+
unless condition.blank?
|
163
|
+
sql = sanitize_sql(condition)
|
164
|
+
segments << sql unless sql.blank?
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
"(#{segments.join(') OR (')})" unless segments.empty?
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
module InstanceMethods
|
173
|
+
|
174
|
+
def latitude_decimal_minutes=(value)
|
175
|
+
unless value.nil?
|
176
|
+
width = value.to_s.length
|
177
|
+
value = value.to_i
|
178
|
+
|
179
|
+
write_attribute :latitude_decimal_minutes, value
|
180
|
+
write_attribute :latitude_decimal_minutes_width, width
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def latitude_decimal_minutes_as_string
|
185
|
+
"%0#{latitude_decimal_minutes_width}d" % latitude_decimal_minutes
|
186
|
+
end
|
187
|
+
|
188
|
+
def longitude_decimal_minutes=(value)
|
189
|
+
unless value.nil?
|
190
|
+
width = value.to_s.length
|
191
|
+
value = value.to_i
|
192
|
+
|
193
|
+
write_attribute :longitude_decimal_minutes, value
|
194
|
+
write_attribute :longitude_decimal_minutes_width, width
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def longitude_decimal_minutes_as_string
|
199
|
+
"%0#{longitude_decimal_minutes_width}d" % longitude_decimal_minutes
|
200
|
+
end
|
201
|
+
|
202
|
+
def latitude
|
203
|
+
to_float latitude_degrees, latitude_minutes, latitude_decimal_minutes,
|
204
|
+
latitude_decimal_minutes_width, latitude_hemisphere
|
205
|
+
end
|
206
|
+
|
207
|
+
def longitude
|
208
|
+
to_float longitude_degrees, longitude_minutes, longitude_decimal_minutes,
|
209
|
+
longitude_decimal_minutes_width, longitude_hemisphere
|
210
|
+
end
|
211
|
+
|
212
|
+
def to_s
|
213
|
+
# Unicode degree symbol, full stop, Unicode minute symbol.
|
214
|
+
units = [ "\xc2\xb0", '.', "\xe2\x80\xb2" ]
|
215
|
+
|
216
|
+
lat_fields = ["%02d" % latitude_degrees,
|
217
|
+
"%02d" % latitude_minutes,
|
218
|
+
latitude_decimal_minutes_as_string.ljust(2, '0'),
|
219
|
+
latitude_hemisphere]
|
220
|
+
lat = lat_fields.zip(units).map{ |f| f.join }.join
|
221
|
+
|
222
|
+
long_fields = ["%02d" % longitude_degrees,
|
223
|
+
"%02d" % longitude_minutes,
|
224
|
+
longitude_decimal_minutes_as_string.ljust(2, '0'),
|
225
|
+
longitude_hemisphere]
|
226
|
+
long = long_fields.zip(units).map{ |f| f.join }.join
|
227
|
+
|
228
|
+
"#{lat}, #{long}"
|
229
|
+
end
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
def to_float(degrees, minutes, decimal_minutes, decimal_minutes_width, hemisphere)
|
234
|
+
return nil if degrees.nil? and minutes.nil? and decimal_minutes.nil?
|
235
|
+
degrees ||= 0
|
236
|
+
minutes ||= 0
|
237
|
+
decimal_minutes ||= 0
|
238
|
+
|
239
|
+
f = degrees.to_f
|
240
|
+
f = f + (minutes.to_f + decimal_minutes.to_f / 10 ** decimal_minutes_width) / 60.0
|
241
|
+
f = f * -1 if hemisphere == 'S' or hemisphere == 'W'
|
242
|
+
f
|
243
|
+
end
|
244
|
+
|
245
|
+
# If some of the fields are empty, set them to zero. This is to speed up data entry.
|
246
|
+
# If all the fields are empty, leave them empty.
|
247
|
+
def set_empty_values
|
248
|
+
unless latitude_degrees.blank? and latitude_minutes.blank? and latitude_decimal_minutes.blank?
|
249
|
+
self.latitude_degrees = 0 if latitude_degrees.blank?
|
250
|
+
self.latitude_minutes = 0 if latitude_minutes.blank?
|
251
|
+
self.latitude_decimal_minutes = 0 if latitude_decimal_minutes.blank?
|
252
|
+
end
|
253
|
+
|
254
|
+
unless longitude_degrees.blank? and longitude_minutes.blank? and longitude_decimal_minutes.blank?
|
255
|
+
self.longitude_degrees = 0 if longitude_degrees.blank?
|
256
|
+
self.longitude_minutes = 0 if longitude_minutes.blank?
|
257
|
+
self.longitude_decimal_minutes = 0 if longitude_decimal_minutes.blank?
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
ActiveRecord::Base.send :include, AirBlade::GeoTools::Location
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module AirBlade
|
2
|
+
module GeoTools
|
3
|
+
module Validations
|
4
|
+
|
5
|
+
# Sames as validates_numericality_of but additionally supports :for option
|
6
|
+
# which lets you attach an error to a different attribute.
|
7
|
+
def validates_inclusion_of_for(*attr_names)
|
8
|
+
configuration = { :on => :save }
|
9
|
+
configuration.update(attr_names.extract_options!)
|
10
|
+
|
11
|
+
enum = configuration[:in] || configuration[:within]
|
12
|
+
|
13
|
+
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
|
14
|
+
|
15
|
+
validates_each(attr_names, configuration) do |record, attr_name, value|
|
16
|
+
unless enum.include?(value)
|
17
|
+
attr_for = configuration[:for] || attr_name
|
18
|
+
record.errors.add(attr_for, :inclusion, :default => configuration[:message], :value => value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sames as validates_numericality_of but additionally supports :for option
|
24
|
+
# which lets you attach an error to a different attribute.
|
25
|
+
def validates_numericality_of_for(*attr_names)
|
26
|
+
configuration = { :on => :save, :only_integer => false, :allow_nil => false }
|
27
|
+
configuration.update(attr_names.extract_options!)
|
28
|
+
|
29
|
+
numericality_options = ActiveRecord::Validations::ClassMethods::ALL_NUMERICALITY_CHECKS.keys & configuration.keys
|
30
|
+
|
31
|
+
(numericality_options - [ :odd, :even ]).each do |option|
|
32
|
+
raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric)
|
33
|
+
end
|
34
|
+
|
35
|
+
validates_each(attr_names,configuration) do |record, attr_name, value|
|
36
|
+
raw_value = record.send("#{attr_name}_before_type_cast") || value
|
37
|
+
|
38
|
+
next if configuration[:allow_nil] and raw_value.nil?
|
39
|
+
|
40
|
+
attr_for = configuration[:for] || attr_name
|
41
|
+
|
42
|
+
if configuration[:only_integer]
|
43
|
+
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
|
44
|
+
record.errors.add(attr_for, :not_a_number, :value => raw_value, :default => configuration[:message])
|
45
|
+
next
|
46
|
+
end
|
47
|
+
raw_value = raw_value.to_i
|
48
|
+
else
|
49
|
+
begin
|
50
|
+
raw_value = Kernel.Float(raw_value)
|
51
|
+
rescue ArgumentError, TypeError
|
52
|
+
record.errors.add(attr_for, :not_a_number, :value => raw_value, :default => configuration[:message])
|
53
|
+
next
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
numericality_options.each do |option|
|
58
|
+
case option
|
59
|
+
when :odd, :even
|
60
|
+
unless raw_value.to_i.method( ActiveRecord::Validations::ClassMethods::ALL_NUMERICALITY_CHECKS[option])[]
|
61
|
+
record.errors.add(attr_for, option, :value => raw_value, :default => configuration[:message])
|
62
|
+
end
|
63
|
+
else
|
64
|
+
record.errors.add(attr_for, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method( ActiveRecord::Validations::ClassMethods::ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
ActiveRecord::Base.send :extend, AirBlade::GeoTools::Validations
|
data/lib/geo_tools.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Treasure < ActiveRecord::Base
|
4
|
+
acts_as_location
|
5
|
+
end
|
6
|
+
|
7
|
+
class GeoToolsTest < ActiveSupport::TestCase
|
8
|
+
|
9
|
+
context 'A location model' do
|
10
|
+
setup { @treasure = Treasure.new }
|
11
|
+
|
12
|
+
should 'convert northern hemisphere latitude fields to a positive float' do
|
13
|
+
@treasure.update_attributes location
|
14
|
+
assert_in_delta 42.95583, @treasure.latitude, 0.0001
|
15
|
+
end
|
16
|
+
|
17
|
+
should 'convert southern hemisphere latitude fields to a negative float' do
|
18
|
+
@treasure.update_attributes location(:latitude_hemisphere => 'S')
|
19
|
+
assert_in_delta -42.95583, @treasure.latitude, 0.0001
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'convert eastern hemisphere longitude fields to a positive float' do
|
23
|
+
@treasure.update_attributes location
|
24
|
+
assert_in_delta 153.37117, @treasure.longitude, 0.0001
|
25
|
+
end
|
26
|
+
|
27
|
+
should 'convert western hemisphere longitude fields to a negative float' do
|
28
|
+
@treasure.update_attributes location(:longitude_hemisphere => 'W')
|
29
|
+
assert_in_delta -153.37117, @treasure.longitude, 0.0001
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'display a pretty #to_s' do
|
33
|
+
@treasure.update_attributes location
|
34
|
+
assert_equal "42°57.35′N, 153°22.27′E", @treasure.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
teardown { Treasure.destroy_all }
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'Location#within' do
|
41
|
+
# TODO: use Factory Girl.
|
42
|
+
|
43
|
+
context 'NE quadrant' do
|
44
|
+
setup do
|
45
|
+
@a = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'N', :longitude_degrees => '153', :longitude_hemisphere => 'E', :latitude_minutes => '12', :longitude_minutes => '47'
|
46
|
+
@b = Treasure.create :latitude_degrees => '43', :latitude_hemisphere => 'N', :longitude_degrees => '153', :longitude_hemisphere => 'E'
|
47
|
+
@c = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'N', :longitude_degrees => '154', :longitude_hemisphere => 'E'
|
48
|
+
end
|
49
|
+
should 'return locations to nearest minute' do
|
50
|
+
assert_same_elements [], Treasure.within(1, 1, 42, 153)
|
51
|
+
assert_same_elements [@a, @b, @c], Treasure.within(1, 1, 43, 154)
|
52
|
+
assert_same_elements [@c], Treasure.within(1, 1, f(42, 11), 154)
|
53
|
+
assert_same_elements [@a, @c], Treasure.within(1, 1, f(42, 12), 154)
|
54
|
+
assert_same_elements [@b], Treasure.within(1, 1, 43, f(153, 46))
|
55
|
+
assert_same_elements [@a, @b], Treasure.within(1, 1, 43, f(153, 47))
|
56
|
+
end
|
57
|
+
teardown { Treasure.destroy_all }
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'NW quadrant' do
|
61
|
+
setup do
|
62
|
+
@a = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'N', :longitude_degrees => '153', :longitude_hemisphere => 'W', :latitude_minutes => '12', :longitude_minutes => '47'
|
63
|
+
@b = Treasure.create :latitude_degrees => '43', :latitude_hemisphere => 'N', :longitude_degrees => '153', :longitude_hemisphere => 'W'
|
64
|
+
@c = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'N', :longitude_degrees => '154', :longitude_hemisphere => 'W'
|
65
|
+
end
|
66
|
+
should 'return locations to nearest minute' do
|
67
|
+
assert_same_elements [], Treasure.within(1, -153, 42, -1)
|
68
|
+
assert_same_elements [@a, @b, @c], Treasure.within(1, -154, 43, -1)
|
69
|
+
assert_same_elements [@c], Treasure.within(1, -154, f(42, 11), -1)
|
70
|
+
assert_same_elements [@a, @c], Treasure.within(1, -154, f(42, 12), -1)
|
71
|
+
assert_same_elements [@b], Treasure.within(1, f(-153, 46), 43, -1)
|
72
|
+
assert_same_elements [@a, @b], Treasure.within(1, f(-153, 47), 43, -1)
|
73
|
+
end
|
74
|
+
teardown { Treasure.destroy_all }
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'SE quadrant' do
|
78
|
+
setup do
|
79
|
+
@a = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'S', :longitude_degrees => '153', :longitude_hemisphere => 'E', :latitude_minutes => '12', :longitude_minutes => '47'
|
80
|
+
@b = Treasure.create :latitude_degrees => '43', :latitude_hemisphere => 'S', :longitude_degrees => '153', :longitude_hemisphere => 'E'
|
81
|
+
@c = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'S', :longitude_degrees => '154', :longitude_hemisphere => 'E'
|
82
|
+
end
|
83
|
+
should 'return locations to nearest minute' do
|
84
|
+
assert_same_elements [], Treasure.within(-42, 1, -1, 153)
|
85
|
+
assert_same_elements [@a, @b, @c], Treasure.within(-43, 1, -1, 154)
|
86
|
+
assert_same_elements [@c], Treasure.within(f(-42, 11), 1, -1, 154)
|
87
|
+
assert_same_elements [@a, @c], Treasure.within(f(-42, 12), 1, -1, 154)
|
88
|
+
assert_same_elements [@b], Treasure.within(-43, 1, -1, f(153, 46))
|
89
|
+
assert_same_elements [@a, @b], Treasure.within(-43, 1, -1, f(153, 47))
|
90
|
+
end
|
91
|
+
teardown { Treasure.destroy_all }
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'SW quadrant' do
|
95
|
+
setup do
|
96
|
+
@a = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'S', :longitude_degrees => '153', :longitude_hemisphere => 'W', :latitude_minutes => '12', :longitude_minutes => '47'
|
97
|
+
@b = Treasure.create :latitude_degrees => '43', :latitude_hemisphere => 'S', :longitude_degrees => '153', :longitude_hemisphere => 'W'
|
98
|
+
@c = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'S', :longitude_degrees => '154', :longitude_hemisphere => 'W'
|
99
|
+
end
|
100
|
+
should 'return locations to nearest minute' do
|
101
|
+
assert_same_elements [], Treasure.within(-42, -153, -1, -1)
|
102
|
+
assert_same_elements [@a, @b, @c], Treasure.within(-43, -154, -1, -1)
|
103
|
+
assert_same_elements [@c], Treasure.within(f(-42, 11), -154, -1, -1)
|
104
|
+
assert_same_elements [@a, @c], Treasure.within(f(-42, 12), -154, -1, -1)
|
105
|
+
assert_same_elements [@b], Treasure.within(-43, f(-153, 46), -1, -1)
|
106
|
+
assert_same_elements [@a, @b], Treasure.within(-43, f(-153, 47), -1, -1)
|
107
|
+
end
|
108
|
+
teardown { Treasure.destroy_all }
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'straddling equator and prime meridian' do
|
112
|
+
setup do
|
113
|
+
@a = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'N', :longitude_degrees => '153', :longitude_hemisphere => 'E', :latitude_minutes => '12', :longitude_minutes => '47'
|
114
|
+
@b = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'N', :longitude_degrees => '153', :longitude_hemisphere => 'W', :latitude_minutes => '12', :longitude_minutes => '47'
|
115
|
+
@c = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'S', :longitude_degrees => '153', :longitude_hemisphere => 'E', :latitude_minutes => '12', :longitude_minutes => '47'
|
116
|
+
@d = Treasure.create :latitude_degrees => '42', :latitude_hemisphere => 'S', :longitude_degrees => '153', :longitude_hemisphere => 'W', :latitude_minutes => '12', :longitude_minutes => '47'
|
117
|
+
end
|
118
|
+
should 'return locations to nearest degree' do
|
119
|
+
assert_same_elements [], Treasure.within(-42, -153, 42, 153)
|
120
|
+
assert_same_elements [@a, @b, @c, @d], Treasure.within(-43, -154, 43, 154)
|
121
|
+
|
122
|
+
assert_same_elements [@a, @b], Treasure.within(f(-42, 11), -154, 43, 154)
|
123
|
+
assert_same_elements [@a, @b, @c, @d], Treasure.within(f(-42, 12), -154, 43, 154)
|
124
|
+
|
125
|
+
assert_same_elements [@a, @c], Treasure.within(-43, f(-153, 46), 43, 154)
|
126
|
+
assert_same_elements [@a, @b, @c, @d], Treasure.within(-43, f(-153, 47), 43, 154)
|
127
|
+
|
128
|
+
assert_same_elements [@c, @d], Treasure.within(-43, -154, f(42, 11), 154)
|
129
|
+
assert_same_elements [@a, @b, @c, @d], Treasure.within(-43, -154, f(42, 12), 154)
|
130
|
+
|
131
|
+
assert_same_elements [@b, @d], Treasure.within(-43, -154, 43, f(153, 46))
|
132
|
+
assert_same_elements [@a, @b, @c, @d], Treasure.within(-43, -154, 43, f(153, 47))
|
133
|
+
end
|
134
|
+
teardown { Treasure.destroy_all }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# TODO: use FactoryGirl instead.
|
142
|
+
|
143
|
+
def location(params = {})
|
144
|
+
{ :latitude_degrees => 42,
|
145
|
+
:latitude_minutes => 57,
|
146
|
+
:latitude_decimal_minutes => 35,
|
147
|
+
:latitude_hemisphere => 'N',
|
148
|
+
:longitude_degrees => 153,
|
149
|
+
:longitude_minutes => 22,
|
150
|
+
:longitude_decimal_minutes => 27,
|
151
|
+
:longitude_hemisphere => 'E' }.merge params
|
152
|
+
end
|
153
|
+
|
154
|
+
# Degrees: positive or negative.
|
155
|
+
# Minutes: always positive.
|
156
|
+
def f(degrees, minutes = 0)
|
157
|
+
degrees >= 0 ? degrees + (minutes / 60.0) : degrees - (minutes / 60.0)
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
data/test/schema.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
create_table :treasures, :force => true do |t|
|
3
|
+
t.string :name
|
4
|
+
t.integer :latitude_degrees, :latitude_minutes, :latitude_decimal_minutes, :latitude_decimal_minutes_width
|
5
|
+
t.string :latitude_hemisphere
|
6
|
+
t.integer :longitude_degrees, :longitude_minutes, :longitude_decimal_minutes, :longitude_decimal_minutes_width
|
7
|
+
t.string :longitude_hemisphere
|
8
|
+
end
|
9
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'shoulda'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
require 'action_view'
|
8
|
+
require 'active_support'
|
9
|
+
require 'active_support/test_case'
|
10
|
+
|
11
|
+
require 'lib/geo_tools'
|
12
|
+
|
13
|
+
ActiveRecord::Base.establish_connection(
|
14
|
+
:adapter => "sqlite3",
|
15
|
+
:database => ":memory:"
|
16
|
+
)
|
17
|
+
load File.dirname(__FILE__) + '/schema.rb'
|
18
|
+
|
19
|
+
class ActiveSupport::TestCase
|
20
|
+
# FIXME: why won't this work?
|
21
|
+
#self.use_transactional_fixtures = true
|
22
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geo_tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andy Stewart
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-16 00:00:00 +00:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: boss@airbladesoftware.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.md
|
24
|
+
files:
|
25
|
+
- MIT-LICENSE
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- VERSION
|
29
|
+
- init.rb
|
30
|
+
- install.rb
|
31
|
+
- lib/air_blade/geo_tools/form_helpers.rb
|
32
|
+
- lib/air_blade/geo_tools/location.rb
|
33
|
+
- lib/air_blade/geo_tools/validations.rb
|
34
|
+
- lib/geo_tools.rb
|
35
|
+
- tasks/geo_tools_tasks.rake
|
36
|
+
- test/geo_tools_test.rb
|
37
|
+
- test/schema.rb
|
38
|
+
- test/test_helper.rb
|
39
|
+
- uninstall.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/airblade/geo_tools
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --charset=UTF-8
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.5
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: View helpers, validations, and named scopes for locations.
|
68
|
+
test_files:
|
69
|
+
- test/geo_tools_test.rb
|
70
|
+
- test/schema.rb
|
71
|
+
- test/test_helper.rb
|