Geocoding is the process of taking latitude and longitude to determine an address, or taking an address and producing latitude and longitude coordinates.
There are numerous reasons an application needs to use a geocoder. For Now Serving, we use it during the sign up process, as well as making it easy to find nearby restaurants with a single click of ‘Find Me’.
Let’s get to coding!
We’ll need to grab the mapbox-sdk
and add it to the Gemfile.
_gem_ **'mapbox-sdk'**, **'~>2'**
Create a simple initializer to set the access token in your app (e.g config/initializers/mapbox.rb
)
**_Mapbox_**.access_token = MAPBOX_ACCESS_TOKEN
Next, let’s add a couple routes:
_namespace_ **:address_search do** _get_ **'expand'**, **to**: **'expand'** _get_ **'parse'**, **to**: **'parse' end**
And an address_search_controller.rb
:
class AddressSearchController < ApplicationControllerTake an addresss and return lat/lng
def expand
begin
@addresses = Mapbox::Geocoder.geocode_forward(address_params[:a]) unless address_params[:a].nil?
render template: ‘address_search/result’
rescue StandardError
render json: { errors: [‘Unable to perform forward geocoding’] }
end
endTake lat/lng array and return a postal address
def parse
begin
@location = { latitude: address_params[:latitude].to_f, longitude: address_params[:longitude].to_f }
@addresses = Mapbox::Geocoder.geocode_reverse(@location)
render template: ‘address_search/result’
rescue StandardError
render json: { errors: [‘Unable to perform reverse geocoding’] }
end
endprivate
def address_params
params.permit(:a, :latitude, :longitude)
end
end
The expand method takes the a
query params and asks the geocoder service to return a latitude/longitude array. For getting an address from lat/lng we are expecting a hash like { latitude: 0, longitude: 0 }
.
You may not want to render a template here but in my case I wanted to always return an array, so the best way to ensure that happened was rendering it with jbuilder one-liner:
json.array! @addresses
And a request spec:
RSpec.describe ‘Address Search’ doit ‘parses an address and returns latitude and longitude’ do
get ‘/api/v1/address_search/expand’, params: { a: ‘401 B St, San Diego CA’ }
expect(response).to be_successful
endit ‘parses latitude and longitude and returns an address’ do
get ‘/api/v1/address_search/parse’, params: { longitude: 127.0, latitude: -43.64}
expect(response).to be_successful
end
end
We’re using the awesome NuxtJS framework for our UI. If you haven’t used it before, definitely give it a look. If you can’t use it, don’t worry; this code will work fine without Nuxt.
We use Vuex actions to call our back end, so we have a store for our Mapbox configuration.
export const actions = {
locate({ commit }, { longitude, latitude }) {
return this.$axios.get(‘/address_search/parse’, { params: { longitude: longitude, latitude: latitude } })
},
coordinate({ commit }, params) {
return this.$axios.get(‘/address_search/expand’, { params: { a: params } })
}
}
For presentation, we use vue-i18n, vue-notify, bootstrap-vue
and vue-fontawesome
.
<template>
<b-btn
v-b-tooltip.hover=“true”
:data-state=“state”
:variant=“btnVariant”
:title=“locationLabel”
type=“button”
@click=“findMe”>
<font-awesome-icon v-if=“state === 1” :icon=“[‘far’, ‘spinner’]” spin />
<font-awesome-icon v-else :icon=“[‘far’, ‘location-arrow’]” />
</b-btn>
</template>
<script>
import { FontAwesomeIcon } from ‘@fortawesome/vue-fontawesome’export default {
components: {
FontAwesomeIcon
},
props: {
locationLabel: {
default: ‘Find my current location’,
type: String
}
},
data() {
return {
state: 0
}
},
computed: {
btnVariant() {
switch (this.state) {
case 0:
return ‘outline-primary’
case 1:
return ‘info’
case 2:
return ‘success’
default:
return ‘outline-primary’
}
}
},
methods: {
findMe() {
const vm = this
this.state = 1
if (!navigator.geolocation) {
vm.$notify({ text: vm.$t(‘geolocation.not_supported’), group: ‘alerts’ })
return
}function success(position) { const accuracy = position.coords.accuracy vm.$store.dispatch('mapbox/locate', { latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: accuracy }) .then((resp) => { vm.state = 2 vm.$emit('result', { name: resp.data[0].features[0].place_name, center: resp.data[0].features[0].center }) }) .catch(() => { vm.state = 0 vm.$notify({ text: vm.$t('geolocation.not_found'), type: 'warning', group: 'alerts' }) }) } function error() { vm.$notify({ text: vm.$t('geolocation.not_found'), group: 'alerts', type: 'warning' }) } navigator.geolocation.getCurrentPosition(success, error) }
}
}
</script>
There’s a lot going on here, so lets break it all down.
The location button has three states; default state, active state, and a success state. The computed property handles changing out the css classes for each state.
There is also a tooltip that displays on hover to explain that the browser will ask for permission to send location information to the back end.
The findMe
method is called on click. In it we have two callbacks for success and error that the browser’s built in [getCurrentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition)
needs to work correctly. When the browser provides the latitude and longitude to the success
callback, we can send that to the back end using the Vuex action. Once the back end response comes, the component emits a result event containing the address name and coordinates. If permission is denied, we display an error notification. Also if the browser doesn’t support location services, we notify the user of that case.
Congrats you have a fully implemented API for forward and reverse geocoding solution!
Originally published by Todd Baur at https://itnext.io
☞ The Complete Ruby on Rails Developer Course
☞ Testing Ruby with RSpec: The Complete Guide
☞ How to build Instagram using Ruby on Rails
☞ Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)
☞ Nuxt JS with Laravel - Build API driven SSR Vue JS Apps
#vue-js #nuxt-js #ruby-on-rails