As part of the UI I’m building around my football stadiums data set I wanted to calculate the distance from a football stadium to a point on the map in Ruby since cypher doesn’t currently return this value.

I had the following cypher query to return the football stadiums near Westminster along with their lat/long values:

lat, long, distance = ["51.55786291569685", "0.144195556640625", 10] query = " START node = node:geom('withinDistance:[#{lat}, #{long}, #{distance}]')" query << " RETURN node.name, node.team, node.lat, node.lon" rows = result["data"].map do |row| { :team => row[1], :stadium => row[0], :lat => row[2], :lon => row[3] } p rows

which returns the following:

[{:team=>"Millwall", :stadium=>"The Den", :lat=>51.4859, :lon=>-0.050743}, {:team=>"Arsenal", :stadium=>"Emirates Stadium", :lat=>51.5549, :lon=>-0.108436}, {:team=>"Chelsea", :stadium=>"Stamford Bridge", :lat=>51.4816, :lon=>-0.191034}, {:team=>"Fulham", :stadium=>"Craven Cottage", :lat=>51.4749, :lon=>-0.221619}, {:team=>"Queens Park Rangers", :stadium=>"Loftus Road", :lat=>51.5093, :lon=>-0.232204}, {:team=>"Leyton Orient", :stadium=>"Brisbane Road", :lat=>51.5601, :lon=>-0.012551}]

In the neo4j spatial code the distance between two points is referred to as the ‘orthodromic distance’ but searching for that didn’t come up with anything. However, I did eventually come across the following postwhich referred to the Haversine formula which is exactly what we want.

There is a good explanation of the formula on the Ask Dr Math forum which defines the formula like so:

dlon = lon2 - lon1 dlat = lat2 - lat1 a = (sin(dlat/2))^2 + cos(lat1) * cos(lat2) * (sin(dlon/2))^2 c = 2 * atan2(sqrt(a), sqrt(1-a)) d = R * c

where:

- R – the radius of the Earth
- c – the great circle distance in radians
- c – the great circle distance in the same units as R
- lat1, lat2, lon1, lon2 – latitude and longitudes in radians

To convert decimal degrees to radians we need to multiply the number of degrees by pi/180 radians/degree.

The Ruby translation of that formula looks like this:

def haversine(lat1, long1, lat2, long2) radius_of_earth = 6378.14 rlat1, rlong1, rlat2, rlong2 = [lat1, long1, lat2, long2].map { |d| as_radians(d)} dlon = rlong1 - rlong2 dlat = rlat1 - rlat2 a = power(Math::sin(dlat/2), 2) + Math::cos(rlat1) * Math::cos(rlat2) * power(Math::sin(dlon/2), 2) great_circle_distance = 2 * Math::atan2(Math::sqrt(a), Math::sqrt(1-a)) radius_of_earth * great_circle_distance end def as_radians(degrees) degrees * Math::PI/180 end def power(num, pow) num ** pow end

And if we change our initial code to use it:

lat, long, distance = ["51.55786291569685", "0.144195556640625", 10] query = " START node = node:geom('withinDistance:[#{lat}, #{long}, #{distance}]')" query << " RETURN node.name, node.team, node.lat, node.lon" rows = result["data"].map do |row| { :team => row[1], :stadium => row[0], :distance => haversine(lat, long, row[2], row[3]).round(2), :lat => row[2], :lon => row[3] } p rows

which gives us the output we want:

[{:team=>"Millwall", :stadium=>"The Den", :distance=>4.87, :lat=>51.4859, :lon=>-0.050743}, {:team=>"Arsenal", :stadium=>"Emirates Stadium", :distance=>5.57, :lat=>51.5549, :lon=>-0.108436}, {:team=>"Chelsea", :stadium=>"Stamford Bridge", :distance=>5.94, :lat=>51.4816, :lon=>-0.191034}, {:team=>"Fulham", :stadium=>"Craven Cottage", :distance=>8.18, :lat=>51.4749, :lon=>-0.221619}, {:team=>"Queens Park Rangers", :stadium=>"Loftus Road", :distance=>8.21, :lat=>51.5093, :lon=>-0.232204}, {:team=>"Leyton Orient", :stadium=>"Brisbane Road", :distance=>9.33, :lat=>51.5601, :lon=>-0.012551}]