radiant-event_map-extension 1.1.0

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