Geocoding using Mapbox, Rails 5 and NuxtJS/VueJS

  • 2019-03-06 02:40 AM
  • 792

Geocoding is the process of taking latitude and longitude to determine an address, or taking an address and producing latitude and longitude coordinates.

rails
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!

Rails API

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 < ApplicationController

  # Take 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
  end

  # Take 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
  end

  private

  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' do

  it '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
  end

  it '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

Front 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.

Conclusion

Congrats you have a fully implemented API for forward and reverse geocoding solution!

Originally published by Todd Baur at https://itnext.io

Learn more

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

Suggest