神刀安全网

Geocoder: Display Maps and Find Places in Rails

Geocoder: Display Maps and Find Places in Rails

The world is big. Seriously, I’d say it’s really huge. Different countries, cities, various people, and cultures…but still, the internet connects us all and that’s really cool. I can communicate with my friends who live a thousand miles away from me.

Because the world is huge, there are many different places that you may need to keep track of within your app. Luckily, there is a great solution to help you find locations by their coordinates, addresses, or even measuring distances between places and finding places nearby. All of this location-based work is called “geocoding”. in Ruby, one geocoding solution is called Geocoder and that’s our guest today.

In this app you will learn how to

  • Integrate Geocoder into your Rails app
  • Tweak Geocoder’s settings
  • Enable geocoding to be able to fetch coordinates based on the address
  • Enable reverse geocoding to grab an address based on the coordinates
  • Measure the distance between locations
  • Add a static map to display the selected location
  • Add a dynamic map to allow users to select a desired location
  • Add the ability to find the location on the map based on coordinates

By the end of the article you will have a solid understanding of Geocoder and a chance to work with the handy Google Maps API. So, shall we start?

The source code is available at GitHub .

The working demo can be found at sitepoint-geocoder.herokuapp.com .

Preparing the App

For this demo, I’ll be using Rails 5 beta 3, but Geocoder supports both Rails 3 and 4. Create a new app called Vagabond (we’ll you don’t really have to call it that, but I find this name somewhat suitable):

$ rails new Vagabond -T 

Suppose we want our users to share places that they have visited. We won’t focus on stuff like authentication, adding photos, videos etc., but you can extend this app yourself later. For now let’s add a table called places with the following fields:

  • title ( string )
  • visited_by ( string ) – later this can be replaced with user_id and marked as a foreign key
  • address ( text ) – address of the place a user has visited
  • latitude and longtitude ( float ) – the exact coordinates of the place. The first draft of the app should fetch them automatically based on the provided address.

Create and apply the appropriate migration:

$ rails g model Place title:string address:text latitude:float longitude:float visited_by:string $ rake db:migrate 

Before moving forward, let’s add bootstrap-rubygem that integrates Bootstrap 4 into our app. I won’t list all the styling in this article, but you can refer to the source code to see the complete markup.

Gemfile

[...] gem 'bootstrap', '~> 4.0.0.alpha3' [...] 

Run

$ bundle install 

Now create a controller, a route, and some views:

places_controller.rb

class PlacesController < ApplicationController   def index     @places = Place.order('created_at DESC')   end    def new     @place = Place.new   end    def create     @place = Place.new(place_params)     if @place.save       flash[:success] = "Place added!"       redirect_to root_path     else       render 'new'     end   end    private    def place_params     params.require(:place).permit(:title, :address, :visited_by)   end end 

config/routes.rb

[...] resources :places, except: [:update, :edit, :destroy] root 'places#index' [...] 

views/places/index.html.erb

<header><h1 class="display-4">Places</h1></header>  <%= link_to 'Add place', new_place_path, class: 'btn btn-primary btn-lg' %>  <div class="card">   <div class="card-block">     <ul>       <%= render @places %>     </ul>   </div> </div> 

views/places/new.html.erb

<header><h1 class="display-4">Add Place</h1></header>  <%= render 'form' %> 

Now the partials:

views/places/_place.html.erb

<li>   <%= link_to place.title, place_path(place) %>   visited by <strong><%= place.visited_by %></strong> </li> 

views/places/_form.html.erb

<%= form_for @place do |f| %>   <fieldset class="form-group">     <%= f.label :title %>     <%= f.text_field :title, class: "form-control" %>   </fieldset>    <fieldset class="form-group">     <%= f.label :visited_by %>     <%= f.text_field :visited_by, class: "form-control" %>   </fieldset>    <fieldset class="form-group">     <%= f.label :address, 'Address' %>     <%= f.text_field :address, class: "form-control" %>   </fieldset>    <%= f.submit 'Add!', class: 'btn btn-primary' %> <% end %> 

We set up the index , new , and create actions for our controller. That’s great, but how are we going to grab coordinates based on the provided address? For that, we’ll utilize Geocoder, so proceed to the next section!

Integrating Geocoder

Add a new gem:

Gemfile

[...] gem 'geocoder' [...] 

and run

$ bundle install 

Starting to work with Geocoder is really simple. Go ahead and add the following line into your model:

models/place.rb

[...] geocoded_by :address [...] 

So, what does it mean? This line equips our model with useful Geocoder methods, that, among others, can be used to retrieve coordinates based on the provided address. The usual place to do that is inside a callback:

models/place.rb

[...] geocoded_by :address after_validation :geocode [...] 

There are a couple of things you have to consider:

  • Your model must present a method that returns the full address – its name is passed as an argument to the geocoded method. In our case that’ll be an address column, but you can use any other method. For example, if you have a separate columns called country , city , and street , the following instance method may be introduced:

    def full_address

    [country, city, street].compact.join(‘, ‘)

    end

Then just pass its name:

geocoded_by :full_address 
  • Your model must also contain two fields called latitude and longitude , with their type set to float . If your columns are called differently, just override the corresponding settings:

    geocoded_by :address, latitude: :lat, longitude: :lon

  • Geocoder supports MongoDB as well, but requires a bit different setup. Read more here and here (overriding coordinates’ names) .

Having these two lines in place, coordinates will be populated automatically based on the provided address. This is possible thanks to Google Geocoding API (though Geocoder supports other options as well – we will talk about it later). What’s more, you don’t even need an API key in order for this to work.

Still, as you’ve probably guessed, the Google API has its usage limits, so we don’t want to query it if the address was unchanged or was not presented at all:

models/place.rb

[...] after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? } [...] 

Now, just add the show action for your PlacesController :

places_controller.rb

[...] def show   @place = Place.find(params[:id]) end [...] 

views/places/show.html.erb

<header><h1 class="display-4"><%= @place.title %></h1></header>  <p>Address: <%= @place.address %></p> <p>Coordinates: <%= @place.latitude %> <%= @place.longitude %></p> 

Boot up your server, provide an address (like “Russia, Moscow, Kremlin”) and navigate to the newly added place. The coordinates should be populated automatically. To check whether they are correct, simply paste them into the search field on this page .

Another interesting thing is that users can even provide IP addresses to detect coordinates – this does not require any changes to the code base at all. Let’s just add a small reminder:

views/places/_form.html.erb

[...] <fieldset class="form-group">   <%= f.label :address, 'Address' %>   <%= f.text_field :address, class: "form-control" %>   <small class="text-muted">You can also enter IP. Your IP is <%= request.ip %></small> </fieldset> [...] 

If you are developing on your local machine, the IP address will be something like ::1 or localhost and obviously won’t be turned into coordinates, but you can provide any other known address ( 8.8.8.8 for Google).

Configuration and APIs

Geocoder supports a bunch of options. To generate a default initializer file, run this command:

$ rails generate geocoder:config 

Inside this file you can set up various things: an API key to use, timeout limit, measurement units to use, and more. Also, you may change the “lookup” providers here. The default values are

:lookup => :google, # for street addresses :ip_lookup => :freegeoip # for IP addresses 

Geocoder’s docs do a great job of listing all possible providers and their usage limits,

so I won’t place them in this article.

One thing to mention is that even though you don’t require an API key to query the Google API, it’s advised to do so because you get an extended quota and also can track the usage of your app. Navigate to the console.developers.google.com , create a new project, and be sure to enable the Google Maps Geocoding API.

Next, just copy the API key and place it inside the initializer file:

config/initializers/geocoder.rb

Geocoder.configure(   api_key: "YOUR_KEY" ) 

Displaying a Static Map

One neat feature about Google Maps is the ability to add static maps (which are essentially images) into your site based on the address or coordinates. Currently, our “show” page does not look very helpful, so let’s add a small map there.

To do that, you will require an API key, so if you did not obtain it in the previous step, do so now. One thing to remember is that the Google Static Maps API has to be enabled.

Now simply tweak your view:

views/places/show.html.erb

[...] <%= image_tag "http://maps.googleapis.com/maps/api/staticmap?center=#{@place.latitude},#{@place.longitude}&markers=#{@place.latitude},#{@place.longitude}&zoom=7&size=640x400&key=AIzaSyA4BHW3txEdqfxzdTlPwaHsYRSZbfeIcd8",               class: 'img-fluid img-rounded', alt: "#{@place.title} on the map"%> 

That’s pretty much it – no JavaScript is required. Static maps support various parameters, like addresses, labels, map styling, and more. Be sure to read the docs .

The page now looks much nicer, but what about the form? It would be much more convenient if users were able to enter not only address but coordinates, as well, by pinpointing the location on an interactive map. Proceed to the next step and let’s do it together!

Adding Support for Coordinates

For now forget about the map – let’s simply allow users to enter coordinates instead of an address. The address itself has to be fetched based on the latitude and longitude. This requires a bit more complex configuration for Geocoder. This approach uses a technique known as “reverse geocoding”.

models/place.rb

[...] reverse_geocoded_by :latitude, :longitude [...] 

This may sound complex, but the idea is simple – we take these two values and grab the address based on it. If your address column is named differently, provide its name like this:

reverse_geocoded_by :latitude, :longitude, :address => :full_address 

Moreover, you can pass a block to this method. It is useful in scenarios when you have separate columns to store country’s and city’s name, street etc.:

reverse_geocoded_by :latitude, :longitude do |obj, results|   if geo = results.first     obj.city    = geo.city     obj.zipcode = geo.postal_code     obj.country = geo.country_code   end end 

More information can be found here .

Now add a callback:

models/place.rb

[...] after_validation :reverse_geocode [...] 

There are a couple of problems though:

  • We don’t want to do reverse geocoding if the coordinates were not provided or modified
  • We don’t want to perform both forward and reverse geocoding
  • We need a separate attribute to store an address provided by the user via the form

The first two issues are easy to solve – just specify the if and unless options:

models/place.rb

[...] after_validation :geocode, if: ->(obj){ obj.address.present? and obj.address_changed? } after_validation :reverse_geocode, unless: ->(obj) { obj.address.present? },                    if: ->(obj){ obj.latitude.present? and obj.latitude_changed? and obj.longitude.present? and obj.longitude_changed? } [...] 

Having this in place, we will fetch coordinates if the address is provided, otherwise try to fetch the address if coordinates are set. But what about a separate attribute for an address? I don’t think we need to add another column – let’s employ a virtual attribute called raw_address instead:

models/place.rb

[...] attr_accessor :raw_address  geocoded_by :raw_address after_validation -> {   self.address = self.raw_address   geocode }, if: ->(obj){ obj.raw_address.present? and obj.raw_address != obj.address }  after_validation :reverse_geocode, unless: ->(obj) { obj.raw_address.present? },                  if: ->(obj){ obj.latitude.present? and obj.latitude_changed? and obj.longitude.present? and obj.longitude_changed? } [...] 

We can utilize this virtual attribute to do geocoding. Don’t forget to update the list of permitted attributes

places_controller.rb

[...] private  def place_params   params.require(:place).permit(:title, :raw_address, :latitude, :longitude, :visited_by) end [...] 

and the view:

views/places/_form.html.erb

<h4>Enter either address or coordinates</h4> <fieldset class="form-group">   <%= f.label :raw_address, 'Address' %>   <%= f.text_field :raw_address, class: "form-control" %>   <small class="text-muted">You can also enter IP. Your IP is <%= request.ip %></small> </fieldset>  <div class="form-group row">   <div class="col-sm-1">     <%= f.label :latitude %>   </div>    <div class="col-sm-3">     <%= f.text_field :latitude, class: "form-control" %>   </div>    <div class="col-sm-1">     <%= f.label :longitude %>   </div>    <div class="col-sm-3">     <%= f.text_field :longitude, class: "form-control" %>   </div> </div> 

So far so good, but without the map, the page still looks uncompleted. On to the next step!

Adding a Dynamic Map

Adding a dynamic map involves some JavaScript, so add it into your layout:

layouts/application.html.erb

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY&callback=initMap"                                        async defer></script> 

Note that the API key is mandatory (be sure to enable “Google Maps JavaScript API”). Also note the callback=initMap parameter. initMap is the function that will be called as soon as this library is loaded, so let’s place it inside the global namespace:

map.coffee

jQuery ->   window.initMap = -> 

Obviously we need a container to place a map into, so add it now:

views/places/_form.html.erb

[...] <div class="card">   <div class="card-block">     <div id="map"></div>   </div> </div> 

The function:

map.coffee

window.initMap = ->   if $('#map').size() > 0     map = new google.maps.Map document.getElementById('map'), {       center: {lat: -34.397, lng: 150.644}       zoom: 8     } 

Note that google.maps.Map requires a JS node to be passed, so this

new google.maps.Map $('#map') 

will not work as $('#map') returns a wrapped jQuery set. To turn it into a JS node, you may say $('#map')[0] .

center is an options that provides the initial position of the map – set the value that works for you.

Now, let’s bind a click event to our map and update the coordinate fields, accordingly.

map.coffee

lat_field = $('#place_latitude') lng_field = $('#place_longitude') [...] window.initMap = ->   map.addListener 'click', (e) ->     updateFields e.latLng [...]  updateFields = (latLng) ->   lat_field.val latLng.lat()   lng_field.val latLng.lng() 

For our users’ convenience, let’s also place a marker at the clicked point. The catch here is that if you click on the map a couple of times, multiple markers will be added, so we have to clear them every time:

map.coffee

markersArray = []  window.initMap = ->   map.addListener 'click', (e) ->     placeMarkerAndPanTo e.latLng, map     updateFields e.latLng  placeMarkerAndPanTo = (latLng, map) ->   markersArray.pop().setMap(null) while(markersArray.length)   marker = new google.maps.Marker     position: latLng     map: map    map.panTo latLng   markersArray.push marker  [...] 

The idea is simple – we store the marker inside the array and remove it on the next click. Having this array, you may keep track of markers that were placed a clear them on some other condition.

It’s high time to test it out. Navigate to the new page and try clicking on the map – the coordinates should be updated properly. That’s much better!

Placing Markers Based on Coordinates

Suppose a user knows coordinates and want to find them on the map instead. This feature is easy to add. Introduce a new “Find on the map” link:

views/places/_form.html.erb

[...] <div class="col-sm-3">   <%= f.text_field :longitude, class: "form-control" %> </div>  <div class="col-sm-4">   <a href="#" id="find-on-map" class="btn btn-info btn-sm">Find on the map</a> </div> [...] 

Now bind a click event to it that updates the map based on the provided coordinates:

map.coffee

[...] window.initMap = ->   $('#find-on-map').click (e) ->     e.preventDefault()     placeMarkerAndPanTo {       lat: parseInt lat_field.val(), 10       lng: parseInt lng_field.val(), 10     }, map [...] 

We pass an object to the placeMarkerAndPanTo function that contains the user-defined latitude and longitude. Note that coordinates have to be converted to integers, otherwise an error will be raised.

Reload the page and check the result! To practice a bit more, you can try to add a similar button for the address field and introduce error handling.

Measuring Distance Between Places

The last thing we will implement today is the ability to measure the distance between added places. Create a new controller:

distances_controller.rb

class DistancesController < ApplicationController   def new     @places = Place.all   end    def create   end end 

Add a route:

config/routes.rb

[...] resources :distances, only: [:new, :create] [...] 

and a view:

views/distances/new.html.erb

<header><h1 class="display-4">Measure Distance</h1></header>  <%= form_tag distances_path do %>   <fieldset class="form-group">   <%= label_tag 'from', 'From' %>   <%= select_tag 'from', options_from_collection_for_select(@places, :id, :title), class: "form-control" %> </fieldset>    <fieldset class="form-group">   <%= label_tag 'to', 'To' %>   <%= select_tag 'to', options_from_collection_for_select(@places, :id, :title), class: "form-control" %> </fieldset>    <%= submit_tag 'Go!', class: 'btn btn-primary' %> <% end %> 

Here we display two drop-downs with our places. options_from_collection_for_select is a handy method that simplifies the generation of option tags. The first argument is the collection, the second – a value to use inside the value option and the last one – the value to display for the user inside the drop-down.

Geocoder allows the measuring of distance between any points on the planet – simply provide their coordinates:

distances_controller.rb

[...] def create   @from = Place.find_by(id: params[:from])   @to = Place.find_by(id: params[:to])   if @from && @to     flash[:success] =         "The distance between <b>#{@from.title}</b> and <b>#{@to.title}</b> is #{@from.distance_from(@to.to_coordinates)} km"   end   redirect_to new_distance_path end [...] 

We find the requested places and use the distance_from method. to_coordinates transforms the record into an array of coordinates (for example, [30.1, -4.3] ) – we have to use it, otherwise the calculation will result in an error.

This method relies on a flash message, so tweak layout a bit:

layouts/application.html.erb

[...] <% flash.each do |name, msg| %>   <%= content_tag(:div, msg.html_safe, class: "alert alert-#{name}") %> <% end %> [...] 

By default Geocoder uses miles as the measurement units, but you can tweak the initializer file and set the units option to km (kilometers) instead.

Conclusion

Phew, that was a long one! We’ve covered many features of Geocoder: forward and reverse geocoding, tweaking options, and measuring distance. On top of that, you learned how to use various types of Google maps and work with them via the API.

Still, there are other features of Geocoder that I have not covered in this article. For example, it supports finding places near the selected location, it can provide directions while measuring distance between locations, it supports caching , and can even be used outside of Rails . If you are planning to use this great gem in your project, be sure to skim the documentation!

That’s all for today folks. Hopefully, this article was useful and interesting for you. Don’t lose your track and see you soon!

Tags:geocoding, Google Maps , Ruby on Rails

Ilya Bodrov is a senior engineer working at Campaigner LLC, teaching assistant at Learnable and lecturer at Russian State Technological University (Internet Technology department). His primary programming languages are Ruby (with Rails) and JavaScript (AngularJS). He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he writes posts for his website , participates in OpenSource projects, goes in for sports and plays music.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Geocoder: Display Maps and Find Places in Rails

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址