radiant-event_map-extension 1.1.0

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.
@@ -0,0 +1,17 @@
1
+ class GetGeocodes < ActiveRecord::Migration
2
+ def self.up
3
+ # [Event, EventVenue].each do |klass|
4
+ # klass.reset_column_information
5
+ # klass.find(:all).each do |this|
6
+ # this.send :geocode_location
7
+ # if this.changed?
8
+ # p "#{this.location} -> #{this.geocode}"
9
+ # this.save!
10
+ # end
11
+ # end
12
+ # end
13
+ end
14
+
15
+ def self.down
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # Uncomment this if you reference any of your controllers in activate
2
+ # require_dependency 'application_controller'
3
+
4
+ class EventMapExtension < Radiant::Extension
5
+ version "1.1.0"
6
+ description "Small additions to geocode calendar events and display on a map, separated here because only of interest to a few."
7
+ url "spanner.org"
8
+
9
+ extension_config do |config|
10
+ config.gem "geokit"
11
+ end
12
+
13
+ def activate
14
+ require 'angle_conversions' # adds String.to_latlng and some degree/radian conversions to Numeric
15
+ require 'grid_ref' # converts from UK grid references to lat/long
16
+ EventVenue.send :include, Mappable # adds geolocation on validation
17
+ end
18
+
19
+ end
@@ -0,0 +1,16 @@
1
+ # Sets up the Rails environment for Cucumber
2
+ ENV["RAILS_ENV"] = "test"
3
+ # Extension root
4
+ extension_env = File.expand_path(File.dirname(__FILE__) + '/../../../../../config/environment')
5
+ require extension_env+'.rb'
6
+
7
+ Dir.glob(File.join(RADIANT_ROOT, "features", "**", "*.rb")).each {|step| require step}
8
+
9
+ Cucumber::Rails::World.class_eval do
10
+ include Dataset
11
+ datasets_directory "#{RADIANT_ROOT}/spec/datasets"
12
+ Dataset::Resolver.default = Dataset::DirectoryResolver.new("#{RADIANT_ROOT}/spec/datasets", File.dirname(__FILE__) + '/../../spec/datasets', File.dirname(__FILE__) + '/../datasets')
13
+ self.datasets_database_dump_path = "#{Rails.root}/tmp/dataset"
14
+
15
+ # dataset :event_map
16
+ end
@@ -0,0 +1,14 @@
1
+ def path_to(page_name)
2
+ case page_name
3
+
4
+ when /the homepage/i
5
+ root_path
6
+
7
+ when /login/i
8
+ login_path
9
+ # Add more page name => path mappings here
10
+
11
+ else
12
+ raise "Can't find mapping from \"#{page_name}\" to a path."
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ class Numeric
2
+ def to_radians # presumes self is a number of degrees
3
+ self * Math::PI / 180
4
+ end
5
+
6
+ def to_degrees # presumes self is a number of radians
7
+ self / (Math::PI / 180)
8
+ end
9
+ end
data/lib/grid_ref.rb ADDED
@@ -0,0 +1,279 @@
1
+ # adapted from Geography::NationalGrid by and (c) P Kent
2
+ # with reference to the Ordnance Survey guide to coordinate systems in the UK
3
+ # http://www.ordnancesurvey.co.uk/oswebsite/gps/information/coordinatesystemsinfo/guidecontents/
4
+ class Ellipsoid
5
+ attr_accessor :a, :b, :e2
6
+
7
+ def initialize(a,b)
8
+ @a = a
9
+ @b = b
10
+ end
11
+
12
+ def ecc
13
+ (a**2 - b**2)/(a**2)
14
+ end
15
+ end
16
+
17
+ class GridRef
18
+ OsTiles = {
19
+ :a => [0,4], :b => [1,4], :c => [2,4], :d => [3,4], :e => [4,4],
20
+ :f => [0,3], :g => [1,3], :h => [2,3], :j => [3,3], :k => [4,3],
21
+ :l => [0,2], :m => [1,2], :n => [2,2], :o => [3,2], :p => [4,2],
22
+ :q => [0,1], :r => [1,1], :s => [2,1], :t => [3,1], :u => [4,1],
23
+ :v => [0,0], :w => [1,0], :x => [2,0], :y => [3,0], :z => [4,0],
24
+ }
25
+ FalseOrigin = [2,1]
26
+ SquareSize = [nil, 10000, 1000, 100, 10, 1] # shorter grid ref = larger square.
27
+
28
+ @@iteration_ceiling = 1000
29
+ @@ellipsoids = {
30
+ :osgb36 => Ellipsoid.new(6377563.396, 6356256.910),
31
+ :wgs84 => Ellipsoid.new(6378137.000, 6356752.3141),
32
+ :ie65 => Ellipsoid.new(6377340.189, 6356034.447),
33
+ :utm => Ellipsoid.new(6378388.000, 6356911.946)
34
+ }
35
+ @@projections = {
36
+ :gb => {:scale => 0.9996012717, :Phio => 49.to_radians, :Lambdao => -2.to_radians, :Eo => 400000, :No => -100000, :ellipsoid => :osgb36},
37
+ :ie => {:scale => 1.000035, :Phio => 53.5.to_radians, :Lambdao => -8.to_radians, :Eo => 250000, :No => 250000, :ellipsoid => :ie65},
38
+ :utm29 => {:scale => 0.9996, :Phio => 0, :Lambdao => -9.to_radians, :Eo => 500000, :No => 0, :ellipsoid => :utm},
39
+ :utm30 => {:scale => 0.9996, :Phio => 0, :Lambdao => -3.to_radians, :Eo => 500000, :No => 0, :ellipsoid => :utm},
40
+ :utm31 => {:scale => 0.9996, :Phio => 0, :Lambdao => 3.to_radians, :Eo => 500000, :No => 0, :ellipsoid => :utm}
41
+ }
42
+ @@helmerts = {
43
+ :wgs84 => { :tx => 446.448, :ty => -125.157, :tz => 542.060, :rx => 0.1502, :ry => 0.2470, :rz => 0.8421, :s => -20.4894 }
44
+ }
45
+
46
+ cattr_accessor :iteration_ceiling
47
+ attr_accessor :gridref, :projection, :ellipsoid, :datum, :options
48
+
49
+ @@defaults = {
50
+ :projection => :gb, # mercator projection of input gridref. Can be any projection name: usually :ie or :gb
51
+ :precision => 6 # decimal places in the output lat/long
52
+ }
53
+
54
+ def initialize(string, options={})
55
+ raise ArgumentError, "invalid grid reference string '#{string}'." unless string.is_gridref?
56
+ @gridref = string.upcase
57
+ @options = @@defaults.merge(options)
58
+ @projection = @@projections[@options[:projection]]
59
+ @ellipsoid = @@ellipsoids[@projection[:ellipsoid]]
60
+ @datum = @options[:datum]
61
+ self
62
+ end
63
+
64
+ def tile
65
+ @tile ||= gridref[0,2]
66
+ end
67
+
68
+ def digits
69
+ @digits ||= gridref[2,10]
70
+ end
71
+
72
+ def resolution
73
+ digits.length / 2
74
+ end
75
+
76
+ def offsets
77
+ if tile
78
+ major = OsTiles[tile[0,1].downcase.to_sym ]
79
+ minor = OsTiles[tile[1,1].downcase.to_sym]
80
+ @offset ||= {
81
+ :e => (500000 * (major[0] - FalseOrigin[0])) + (100000 * minor[0]),
82
+ :n => (500000 * (major[1] - FalseOrigin[1])) + (100000 * minor[1])
83
+ }
84
+ else
85
+ { :e => 0, :n => 0 }
86
+ end
87
+ end
88
+
89
+ def easting
90
+ @east ||= offsets[:e] + digits[0, resolution].to_i * SquareSize[resolution]
91
+ end
92
+
93
+ def northing
94
+ @north ||= offsets[:n] + digits[resolution, resolution].to_i * SquareSize[resolution]
95
+ end
96
+
97
+ def lat
98
+ coordinates[:lat].to_degrees.round(self.options[:precision])
99
+ end
100
+
101
+ def lng
102
+ coordinates[:lng].to_degrees.round(self.options[:precision])
103
+ end
104
+
105
+ def to_s
106
+ gridref.to_s
107
+ end
108
+
109
+ def to_latlng
110
+ "#{lat}, #{lng}"
111
+ end
112
+
113
+ def coordinates
114
+ unless @coordinates
115
+ # variable names correspond roughly to symbols in the OS algorithm, lowercased:
116
+ # n0 = northing of true origin
117
+ # e0 = easting of true origin
118
+ # f0 = scale factor on central meridian
119
+ # phi0 = latitude of true origin
120
+ # lambda0 = longitude of true origin and central meridian.
121
+ # e2 = eccentricity squared
122
+ # a = length of polar axis of ellipsoid
123
+ # b = length of equatorial axis of ellipsoid
124
+ # ning & eing are the northings and eastings of the supplied gridref
125
+ # phi and lambda are the discovered latitude and longitude
126
+
127
+ ning = northing
128
+ eing = easting
129
+
130
+ n0 = projection[:No]
131
+ e0 = projection[:Eo]
132
+ phi0 = projection[:Phio]
133
+ l0 = projection[:Lambdao]
134
+ f0 = projection[:scale]
135
+
136
+ a = ellipsoid.a
137
+ b = ellipsoid.b
138
+ e2 = ellipsoid.ecc
139
+
140
+ # the rest is taken from the OS equations with help from CPAN's Geography::NationalGrid
141
+ # and only enough understanding to transliterate it, and sometimes not even that.
142
+
143
+ n = (a - b) / (a + b)
144
+ m = 0
145
+ phi = phi0
146
+
147
+ # iterate to within acceptable distance of solution
148
+
149
+ count = 0
150
+ while ((ning - n0 - m) >= 0.001) do
151
+ raise RuntimeError "Demercatorising equation has not converged. Discrepancy after #{count} cycles is #{ning - n0 - m}" if count >= @@iteration_ceiling
152
+
153
+ phi = ((ning - n0 - m) / (a * f0)) + phi
154
+ ma = (1 + n + (1.25 * n**2) + (1.25 * n**3)) * (phi - phi0)
155
+ mb = ((3 * n) + (3 * n**2) + (2.625 * n**3)) * Math.sin(phi - phi0) * Math.cos(phi + phi0)
156
+ mc = ((1.875 * n**2) + (1.875 * n**3)) * Math.sin(2 * (phi - phi0)) * Math.cos(2 * (phi + phi0))
157
+ md = (35/24) * (n**3) * Math.sin(3 * (phi - phi0)) * Math.cos(3 * (phi + phi0))
158
+ m = b * f0 * (ma - mb + mc - md)
159
+ count += 1
160
+ end
161
+
162
+ # engage alphabet soup
163
+
164
+ nu = a * f0 * ((1-(e2) * ((Math.sin(phi)**2))) ** -0.5)
165
+ rho = a * f0 * (1-(e2)) * ((1-(e2)*((Math.sin(phi)**2))) ** -1.5)
166
+ eta2 = (nu/rho - 1)
167
+
168
+ # fire
169
+
170
+ vii = Math.tan(phi) / (2 * rho * nu)
171
+ viii = (Math.tan(phi) / (24 * rho * (nu ** 3))) * (5 + (3 * (Math.tan(phi) ** 2)) + eta2 - 9 * eta2 * (Math.tan(phi) ** 2) )
172
+ ix = (Math.tan(phi) / (720 * rho * (nu ** 5))) * (61 + (90 * (Math.tan(phi) ** 2)) + (45 * (Math.tan(phi) ** 4)) )
173
+ x = sec(phi) / nu
174
+ xi = (sec(phi) / (6 * nu ** 3)) * ((nu/rho) + 2 * (Math.tan(phi) ** 2))
175
+ xii = (sec(phi) / (120 * nu ** 5)) * (5 + (28 * (Math.tan(phi) ** 2)) + (24 * (Math.tan(phi) ** 4)))
176
+ xiia = (sec(phi) / (5040 * nu ** 7)) * (61 + (662 * (Math.tan(phi) ** 2)) + (1320 * (Math.tan(phi) ** 4)) + (720 * (Math.tan(phi) ** 6)))
177
+
178
+ d = eing-e0
179
+
180
+ # all of which was just to populate these last two equations:
181
+
182
+ phi = phi - vii*(d**2) + viii*(d**4) - ix*(d**6)
183
+ lambda = l0 + x*d - xi*(d**3) + xii*(d**5) - xiia*(d**7)
184
+
185
+ # here coordinates are still in radians and osgb36
186
+
187
+ @coordinates = helmerted(phi, lambda)
188
+ end
189
+
190
+ @coordinates
191
+ end
192
+
193
+ def helmerted(phi, lambda)
194
+ return {:lat => phi, :lng => lambda} unless @datum && @datum != :osgb36
195
+ target_datum = @@ellipsoids[@datum]
196
+ t = @@helmerts[@datum]
197
+
198
+ if t && target_datum
199
+
200
+ # convert polar to cartesian coordinates using osgb datum
201
+
202
+ a = @@ellipsoids[:osgb36].a
203
+ b = @@ellipsoids[:osgb36].b
204
+ e2 = @@ellipsoids[:osgb36].ecc
205
+
206
+ nu = a / (Math.sqrt(1 - e2 * Math.sin(phi)**2))
207
+ h = 0
208
+
209
+ x1 = (nu + h) * Math.cos(phi) * Math.cos(lambda)
210
+ y1 = (nu + h) * Math.cos(phi) * Math.sin(lambda)
211
+ z1 = ((1 - e2) * nu + h) * Math.sin(phi)
212
+
213
+ # parameterise helmert transformation
214
+
215
+ tx = t[:tx]
216
+ ty = t[:ty]
217
+ tz = t[:tz]
218
+ rx = (t[:rx]/3600).to_radians
219
+ ry = (t[:ry]/3600).to_radians
220
+ rz = (t[:rz]/3600).to_radians
221
+ s1 = t[:s]/1e6 + 1
222
+
223
+ # apply helmert transformation
224
+
225
+ xp = tx + x1*s1 - y1*rz + z1*ry
226
+ yp = ty + x1*rz + y1*s1 - z1*rx
227
+ zp = tz - x1*ry + y1*rx + z1*s1
228
+
229
+ # convert back to polar coordinates using target datum
230
+
231
+ a = target_datum.a
232
+ b = target_datum.b
233
+ e2 = target_datum.ecc
234
+ precision = 4 / a
235
+
236
+ p = Math.sqrt(xp**2 + yp**2)
237
+ phi = Math.atan2(zp, p*(1-e2));
238
+ phip = 2 * Math::PI
239
+
240
+ count = 0
241
+ while (phi-phip).abs > precision do
242
+ raise RuntimeError "Helmert transformation has not converged. Discrepancy after #{count} cycles is #{phi-phip}" if count >= @@iteration_ceiling
243
+
244
+ nu = a / Math.sqrt(1 - e2 * Math.sin(phi)**2)
245
+ phip = phi
246
+ phi = Math.atan2(zp + e2 * nu * Math.sin(phi), p)
247
+ count += 1
248
+ end
249
+
250
+ lambda = Math.atan2(yp, xp)
251
+
252
+ {:lat => phi, :lng => lambda}
253
+
254
+ else
255
+ raise RuntimeError, "Missing ellipsoid or Helmert transformation for #{@datum}"
256
+ end
257
+ end
258
+
259
+ private
260
+
261
+ def sec(radians)
262
+ 1 / Math.cos(radians)
263
+ end
264
+
265
+ end
266
+
267
+ class String
268
+ def is_gridref?
269
+ self.upcase =~ /^(H(P|T|U|Y|Z)|N(A|B|C|D|F|G|H|J|K|L|M|N|O|R|S|T|U|W|X|Y|Z)|OV|S(C|D|E|G|H|J|K|M|N|O|P|R|S|T|U|W|X|Y|Z)|T(A|F|G|L|M|Q|R|V)){1}\d{4}(NE|NW|SE|SW)?$|((H(P|T|U|Y|Z)|N(A|B|C|D|F|G|H|J|K|L|M|N|O|R|S|T|U|W|X|Y|Z)|OV|S(C|D|E|G|H|J|K|M|N|O|P|R|S|T|U|W|X|Y|Z)|T(A|F|G|L|M|Q|R|V)){1}(\d{4}|\d{6}|\d{8}|\d{10}))$/
270
+ end
271
+
272
+ def to_latlng(options = {})
273
+ GridRef.new(self, options).to_latlng if self.is_gridref?
274
+ end
275
+
276
+ def to_wgs84(options = {})
277
+ GridRef.new(self, options.merge(:datum => :wgs84)).to_latlng if self.is_gridref?
278
+ end
279
+ end
data/lib/mappable.rb ADDED
@@ -0,0 +1,63 @@
1
+ module Mappable
2
+ include Geokit::Geocoders
3
+
4
+ def self.included(base)
5
+ base.class_eval {
6
+ before_validation :geocode_location
7
+ }
8
+ end
9
+
10
+ def geocode
11
+ "#{lat},#{lng}" if geocoded
12
+ end
13
+
14
+ def geocoded
15
+ true if lat && lng
16
+ end
17
+
18
+ def geocode_basis
19
+ geocodable_columns.map{ |f| send(f) }.find{|v| !v.blank?}
20
+ end
21
+
22
+ def geocodable_columns
23
+ [:postcode, :location, :address]
24
+ end
25
+
26
+ def url
27
+ if url = read_attribute(:url) && !url.blank?
28
+ url
29
+ elsif geocoded
30
+ format = Radiant::Config['event_map.link_format']
31
+ format = 'google' if format.blank?
32
+ case format
33
+ when 'bing'
34
+ "http://www.bing.com/maps/?v=2&cp=#{lat}~#{lng}&rtp=~pos.#{lat}_#{lng}_#{title}&lvl=15&sty=s&eo=0"
35
+ when 'google'
36
+ "http://maps.google.com/maps?q=#{lat}+#{lng}+(#{title})"
37
+ when String
38
+ interpolations = %w{lat lng title}
39
+ interpolations.inject( format.dup ) do |result, tag|
40
+ result.gsub(/:#{tag}/) { send( tag ) }
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def geocode_location
49
+ unless geocode_basis.blank? || ENV['RAILS_ENV'] == 'test'
50
+ if geocode_basis.is_gridref? && gr = GridRef.new(geocode_basis, :datum => :wgs84, :accuracy => 8) # for gps and site compatibility wgs84 is better than osgb36
51
+ self.lat = gr.lat
52
+ self.lng = gr.lng
53
+ puts "new lat/lng: #{self.lat}/#{self.lng}"
54
+ else
55
+ bias = Radiant::Config['event_map.zone'] || 'uk'
56
+ geo = Geokit::Geocoders::MultiGeocoder.geocode(location, :bias => bias)
57
+ errors.add(:postcode, "Could not Geocode location: please specify here") if !geo.success
58
+ self.lat, self.lng = geo.lat, geo.lng if geo.success
59
+ end
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,28 @@
1
+ namespace :radiant do
2
+ namespace :extensions do
3
+ namespace :event_map do
4
+
5
+ desc "Runs the migration of the Event Map extension"
6
+ task :migrate => :environment do
7
+ require 'radiant/extension_migrator'
8
+ if ENV["VERSION"]
9
+ EventMapExtension.migrator.migrate(ENV["VERSION"].to_i)
10
+ else
11
+ EventMapExtension.migrator.migrate
12
+ end
13
+ end
14
+
15
+ desc "Copies public assets of the Event Map to the instance public/ directory."
16
+ task :update => :environment do
17
+ is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
18
+ puts "Copying assets from EventMapExtension"
19
+ Dir[EventMapExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
20
+ path = file.sub(EventMapExtension.root, '')
21
+ directory = File.dirname(path)
22
+ mkdir_p RAILS_ROOT + directory, :verbose => false
23
+ cp file, RAILS_ROOT + path, :verbose => false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end