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