Статьи

Геокодер: Показать карты и найти места в Rails

Мир большой. Серьезно, я бы сказал, что это действительно огромный. Разные страны, города, разные люди и культуры … но, тем не менее, интернет связывает нас всех, и это действительно круто. Я могу общаться с моими друзьями, которые живут за тысячу миль от меня.

Поскольку мир огромен, есть много разных мест, которые вы, возможно, должны отслеживать в своем приложении. К счастью, есть отличное решение, которое поможет вам находить местоположения по их координатам, адресам или даже измерять расстояния между местами и находить места поблизости. Вся эта работа на основе местоположения называется «геокодирование». в Ruby одно решение геокодирования называется Geocoder, и это наш гость сегодня.

В этом приложении вы узнаете, как

  • Интегрируйте Geocoder в ваше приложение Rails
  • Настройки твика геокодера
  • Включить геокодирование, чтобы можно было получать координаты на основе адреса
  • Включить обратное геокодирование, чтобы получить адрес на основе координат
  • Измерьте расстояние между локациями
  • Добавить статическую карту для отображения выбранного местоположения
  • Добавьте динамическую карту, чтобы позволить пользователям выбрать желаемое местоположение
  • Добавить возможность найти местоположение на карте на основе координат

К концу статьи вы получите четкое представление о Geocoder и сможете поработать с удобным API Карт Google. Итак, начнем?

Исходный код доступен на GitHub .

Рабочую демонстрацию можно найти по адресу sitepoint-geocoder.herokuapp.com .

Подготовка приложения

Для этой демонстрации я буду использовать Rails 5 beta 3, но Geocoder поддерживает оба Rails 3 и 4. Создайте новое приложение под названием Vagabond (мы действительно не будем так его называть, но я несколько нахожу это название подходящее):

$ rails new Vagabond -T 

Предположим, мы хотим, чтобы наши пользователи делились местами, которые они посетили. Мы не будем фокусироваться на таких вещах, как аутентификация, добавление фотографий, видео и т. Д., Но вы можете расширить это приложение позже. А пока давайте добавим таблицу с именем places со следующими полями:

  • title ( string )
  • visited_by ( string ) — позже это можно заменить на user_id и пометить как внешний ключ
  • address ( text ) — адрес места, которое посетил пользователь
  • latitude и longtitude ( float ) — точные координаты места. Первый черновик приложения должен получать их автоматически на основе указанного адреса.

Создайте и примените соответствующую миграцию:

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

Прежде чем двигаться вперед, давайте добавим bootstrap-rubygem, который интегрирует Bootstrap 4 в наше приложение. Я не буду перечислять все стили в этой статье, но вы можете обратиться к исходному коду, чтобы увидеть полную разметку.

Gemfile

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

Бегать

 $ bundle install 

Теперь создайте контроллер, маршрут и некоторые виды:

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 

конфиг / routes.rb

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

просмотры / места / 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> 

просмотры / места / new.html.erb

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

Теперь частичные:

просмотры / места / _place.html.erb

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

просмотры / места / _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 %> 

Мы устанавливаем index , new и create действия для нашего контроллера. Это здорово, но как мы собираем координаты на основе предоставленного адреса? Для этого мы будем использовать Geocoder, поэтому перейдите к следующему разделу!

Интегрирующий геокодер

Добавьте новый драгоценный камень:

Gemfile

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

и беги

 $ bundle install 

Начать работать с Geocoder действительно просто. Идите вперед и добавьте следующую строку в вашу модель:

модели / place.rb

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

Итак, что это значит? Эта строка снабжает нашу модель полезными методами геокодера, которые, среди прочего, могут использоваться для получения координат на основе предоставленного адреса. Обычное место для этого — внутри обратного вызова:

модели / place.rb

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

Есть несколько вещей, которые вы должны рассмотреть:

  • Ваша модель должна представить метод, который возвращает полный адрес — его имя передается в качестве аргумента geocoded методу. В нашем случае это будет address столбец, но вы можете использовать любой другой метод. Например, если у вас есть отдельные столбцы с названием country , city и street , можно ввести следующий метод экземпляра:

    def full_address
    [страна, город, улица] .compact.join (‘,’)
    конец

Тогда просто передайте его имя:

 geocoded_by :full_address 
  • Ваша модель также должна содержать два поля, называемых latitude и longitude , а их тип должен быть float . Если ваши столбцы называются по-разному, просто переопределите соответствующие настройки:

    geocoded_by: адрес, широта:: широта, долгота:: долгота

  • Geocoder также поддерживает MongoDB, но требует немного другой настройки. Читайте больше здесь и здесь (переопределение имен координат) .

При наличии этих двух строк координаты будут автоматически заполняться на основе указанного адреса. Это возможно благодаря Google Geocoding API (хотя Geocoder поддерживает и другие опции — мы поговорим об этом позже). Более того, вам даже не нужен ключ API, чтобы это работало.

Тем не менее, как вы уже, наверное, догадались, API Google имеет свои ограничения использования, поэтому мы не хотим запрашивать его, если адрес был неизменным или не был представлен вообще:

модели / place.rb

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

Теперь просто добавьте действие show для вашего PlacesController :

places_controller.rb

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

просмотры / места / show.html.erb

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

Загрузите свой сервер, укажите адрес (например, «Россия, Москва, Кремль») и перейдите к новому добавленному месту. Координаты должны быть заполнены автоматически. Чтобы проверить, верны ли они, просто вставьте их в поле поиска на этой странице .

Другая интересная вещь заключается в том, что пользователи могут даже предоставлять IP-адреса для определения координат — это вообще не требует каких-либо изменений в кодовой базе. Давайте просто добавим небольшое напоминание:

просмотры / места / _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> [...] 

Если вы разрабатываете на локальном компьютере, IP-адрес будет примерно таким ::1 или localhost и, очевидно, не будет преобразован в координаты, но вы можете 8.8.8.8 любой другой известный адрес ( 8.8.8.8 для Google).

Конфигурация и API

Геокодер поддерживает множество вариантов. Чтобы создать файл инициализатора по умолчанию, выполните эту команду:

 $ rails generate geocoder:config 

Внутри этого файла вы можете настроить различные вещи: ключ API для использования, предел времени ожидания, единицы измерения и многое другое. Кроме того, вы можете изменить провайдеров «поиска» здесь. Значения по умолчанию

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

Документы Geocoder отлично справляются со списком всех возможных провайдеров и их лимитов использования,
поэтому я не буду помещать их в эту статью.

Следует упомянуть, что, хотя вам не требуется ключ API для запроса API Google, рекомендуется делать это, потому что вы получаете расширенную квоту и также можете отслеживать использование вашего приложения. Перейдите на console.developers.google.com , создайте новый проект и обязательно включите API геокодирования Карт Google.

Далее просто скопируйте ключ API и поместите его в файл инициализатора:

конфиг / Инициализаторы / geocoder.rb

 Geocoder.configure( api_key: "YOUR_KEY" ) 

Отображение статической карты

Одна из полезных функций в Картах Google — это возможность добавлять статические карты (которые по сути являются изображениями) на ваш сайт на основе адреса или координат. В настоящее время наша страница показа выглядит не очень полезной, поэтому давайте добавим туда небольшую карту.

Для этого вам потребуется ключ API, поэтому, если вы не получили его на предыдущем шаге, сделайте это сейчас. Следует помнить, что API Статических Карт Google должен быть включен.

Теперь просто настройте ваш взгляд:

просмотры / места / 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"%> 

Вот и все — JavaScript не требуется. Статические карты поддерживают различные параметры, такие как адреса, метки, стиль карты и многое другое. Обязательно прочитайте документы .

Страница теперь выглядит намного лучше, но как насчет формы? Было бы гораздо удобнее, если бы пользователи могли вводить не только адрес, но и координаты, точно определяя местоположение на интерактивной карте. Перейдите к следующему шагу, и давайте сделаем это вместе!

Добавление поддержки для координат

А пока забудьте о карте — давайте просто позволим пользователям вводить координаты вместо адреса. Сам адрес должен быть выбран на основе широты и долготы. Это требует немного более сложной конфигурации для геокодера. Этот подход использует метод, известный как «обратное геокодирование».

модели / place.rb

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

Это может показаться сложным, но идея проста — мы берем эти два значения и получаем адрес, основанный на нем. Если ваш адресный столбец назван по-другому, укажите его имя так:

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

Более того, вы можете передать блок этому методу. Это полезно в сценариях, когда у вас есть отдельные столбцы для хранения названия страны и города, улицы и т. Д .:

 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 

Более подробную информацию можно найти здесь .

Теперь добавьте обратный вызов:

модели / place.rb

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

Хотя есть пара проблем:

  • Мы не хотим делать обратное геокодирование, если координаты не были предоставлены или изменены
  • Мы не хотим выполнять прямое и обратное геокодирование
  • Нам нужен отдельный атрибут для хранения адреса, предоставленного пользователем через форму

Первые две проблемы легко решить — просто укажите параметры if и unless :

модели / 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? } [...] 

Имея это на месте, мы будем выбирать координаты, если указан адрес, в противном случае попробуйте получить адрес, если координаты установлены. Но как насчет отдельного атрибута для адреса? Я не думаю, что нам нужно добавлять еще один столбец — давайте использовать вместо этого виртуальный атрибут raw_address :

модели / 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? } [...] 

Мы можем использовать этот виртуальный атрибут для геокодирования. Не забудьте обновить список разрешенных атрибутов

places_controller.rb

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

и вид:

просмотры / места / _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> 

Пока все хорошо, но без карты страница все еще выглядит незавершенной. На следующем шаге!

Добавление динамической карты

Добавление динамической карты требует некоторого JavaScript, поэтому добавьте его в свой макет:

макеты / application.html.erb

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

Обратите внимание, что ключ API является обязательным (обязательно включите «Google Maps JavaScript API»). Также обратите внимание на параметр callback=initMap . initMap — это функция, которая будет вызываться, как только эта библиотека будет загружена, поэтому давайте initMap ее в глобальное пространство имен:

map.coffee

 jQuery -> window.initMap = -> 

Очевидно, нам нужен контейнер для размещения карты, поэтому добавьте его сейчас:

просмотры / места / _form.html.erb

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

Функция:

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 } 

Обратите внимание, что google.maps.Map требует передачи узла JS, поэтому

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

не будет работать, так как $('#map') возвращает упакованный набор jQuery. Чтобы превратить его в узел JS, вы можете сказать $('#map')[0] .

center — это опция, которая обеспечивает начальное положение карты — установите значение, которое работает для вас.

Теперь давайте свяжем событие click с нашей картой и обновим поля координат соответственно.

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() 

Для удобства наших пользователей давайте также разместим маркер в точке нажатия. Подвох в том, что если вы нажмете на карту пару раз, будет добавлено несколько маркеров, поэтому мы должны каждый раз очищать их:

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 [...] 

Идея проста — мы сохраняем маркер внутри массива и удаляем его при следующем нажатии. Имея этот массив, вы можете отслеживать маркеры, которые были помещены, чтобы очистить их при некоторых других условиях.

Самое время это проверить. Перейдите на новую страницу и попробуйте нажать на карту — координаты должны быть обновлены правильно. Это гораздо лучше!

Размещение маркеров на основе координат

Предположим, что пользователь знает координаты и хочет найти их на карте. Эту функцию легко добавить. Введите новую ссылку «Найти на карте»:

просмотры / места / _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> [...] 

Теперь привяжите к нему событие click которое обновит карту на основе предоставленных координат:

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 [...] 

Мы передаем объект в функцию placeMarkerAndPanTo которая содержит определяемые пользователем широту и долготу. Обратите внимание, что координаты должны быть преобразованы в целые числа, в противном случае возникнет ошибка.

Перезагрузите страницу и проверьте результат! Чтобы попрактиковаться немного, вы можете попробовать добавить аналогичную кнопку для поля адреса и ввести обработку ошибок.

Измерение расстояния между местами

Последнее, что мы будем реализовывать сегодня, — это возможность измерять расстояние между добавленными местами. Создайте новый контроллер:

distances_controller.rb

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

Добавить маршрут:

конфиг / routes.rb

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

и вид:

просмотры / расстояние / 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 %> 

Здесь мы показываем два раскрывающихся списка с нашими местами. options_from_collection_for_select — удобный метод, который упрощает генерацию тегов option . Первый аргумент — это коллекция, второй — значение для использования внутри параметра value а последний — значение, отображаемое для пользователя в раскрывающемся списке.

Геокодер позволяет измерять расстояние между любыми точками на планете — просто укажите их координаты:

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 [...] 

Мы находим запрошенные места и используем метод distance_from . to_coordinates преобразует запись в массив координат (например, [30.1, -4.3] ) — мы должны использовать его, иначе вычисление приведет к ошибке.

Этот метод основан на флеш-сообщении, поэтому немного подправьте макет:

макеты / application.html.erb

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

По умолчанию Geocoder использует мили в качестве единиц измерения, но вы можете настроить файл инициализатора и установить вместо него units в km (километры).

Вывод

Фу, это было долго! Мы рассмотрели многие функции геокодера: прямое и обратное геокодирование, параметры настройки и измерение расстояния. Кроме того, вы узнали, как использовать различные типы карт Google и работать с ними через API.

Тем не менее, есть и другие функции Geocoder, которые я не рассмотрел в этой статье. Например, он поддерживает поиск мест рядом с выбранным местоположением, может указывать направления при измерении расстояния между местоположениями, поддерживает кэширование и может даже использоваться вне Rails . Если вы планируете использовать этот драгоценный камень в своем проекте, обязательно ознакомьтесь с документацией!

Это все на сегодня, ребята. Надеюсь, эта статья была полезной и интересной для вас. Не теряйте свой след и до скорой встречи!