In this post, I will be implementing a solution to find the estimated distance of a user to Places saved in my application.
For simplicity's sake, let's imagine we have a web app that displays many restaurants from various cities and states. The web app is connected to a backend service in Rails with a relational database. Before the implementation of the geolocation feature, restaurants were displayed in a random order, which means a user might see a restaurant from another state before seeing one they are more likely to visit.
The requirements for the feature are:
- When a user accesses the app, they will have the option to share their geolocation.
- Upon accepting, the page should be more personalized based on the distance of the user to the restaurants.
- The location doesn't have to be precise, like GPS precision, but it shouldn't be imprecise within a 0.5 km margin.
Collecting the user's location
In the context of a web-app, the easiest way to collect a user's estimated location is using the browser's Geolocation API.
Calling the getCurrentPosition
or watchCurrentPosition
functions will prompt the user to allow or deny sharing their geolocation
Here is a sample implementation that should prompt the user to share their Geolocation, saves it to localStorage
and handles errors with console.log
const getGeolocation = () => {
if (navigator.geolocation) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
const location = {
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
resolve(localStorage.setItem('geolocation', JSON.stringify(location)));
},
(err) => {
if (err.code === 1) {
reject(console.log('User denied the request for Geolocation.'));
} else {
reject(console.log('An error occurred while retrieving location.'));
}
},
);
});
}
return Promise.resolve(console.log('Geolocation is not supported by this browser.'));
};
The data retrieved by this function is the user's approximate latitude and longitude. Keep in mind, this location data can be derived from various sources such as GPS, WiFi, or the user's internet connection network, so it may not always be highly precise. However, it is accurate enough for our requirements and for most e-commerce apps.
You can then send these coordinates to your backend, where you can handle the proximity of the restaurants with an appropriate query.
Sorting Records by Geolocation
The defined requirement is that the page should be more personalized based on location. One way to do that is by ordering the Restaurants by proximity.
Now consider that we have a Restaurants
table with address information, such as city, state, street address, but no coordinates. How can we find the distance between coordinates and an address?
There are ways to find the distance between two coordinates, such as the Haversine formula, which is a trigonometric function to find the distance between two points in a perfect sphere. The earth isn't exactly a sphere, but given our precision requirements it's close enough to a sphere. If you want to read more about a precise calculation of Geolocation, you can checkout Postgis.
A possible implementation of the Haversine formula looks like this:
def self.order_by_distance(lat, lon)
select("restaurants.*,
(6371 * acos(cos(radians(#{lat})) * cos(radians(latitude)) *
cos(radians(longitude) - radians(#{lon})) +
sin(radians(#{lat})) * sin(radians(latitude)))) AS distance")
.order("distance")
end
This is an active record query on the Restaurant model that takes as argument latitude and longitude and compares it to that of all Restaurant records, calculating the distance. It then orders restaurants by the closest ones.
It is based off of this post on applying Haversine to SQL queries.
Luckily, the Postgres team has already implemented this, and it can be used by adding the earthdistance extension.
If you're using Rails and Postgres, you would need a migration like this
class AddsEarthdistanceExtensionToPostgres < ActiveRecord::Migration[7.0]
def change
enable_extension 'cube' #a dependency of the earthdistance extension
enable_extension 'earthdistance'
end
end
The previous query with the extension looks like this:
def self.order_by_distance(lat, lon)
select("restaurants.*,
earth_distance(ll_to_earth(#{lat}, #{lon}),
ll_to_earth(latitude, longitude))
AS distance")
.order('distance')
end
The ll_to_earth
function, transforms latitude and longitude values to spatial coordinates, and the earth_distance
function calculates the distance between these coordinates using the Haversine formula.
This formula is also implemented in many other databases, for instance Redis GEODIST.
Address to coordinates
Ok, now we know how to find the distance between coordinates, and we know how to find the user's coordinates, but we don't have the Restaurant's coordinates. How do we get them?
There are many apis that can be used to get the coordinates of an address. Most of them are freemium services. Here are some of the most popular ones:
You can test a few records yourself using Google Maps on their web page to see how it works. In my implementation, I chose Google Maps because it handles various address formats and styles more flexibly than other apis.
That's it, now we have everything we need to implement our geolocation feature, just plug these coordinates into your Restaurants and we are good to go.
References
Geolocation API.
Haversine formula
Postindustria geo queries made easy
Postgis
Redis GEODIST
Google Maps Geocoding API
OpenStreetMap