📜 ⬆️ ⬇️

Ruby and Postgis Time Zone Service

In one of the projects in which I participated, the task arose of determining the time zone by the current geolocation of the user. On the backend came a record created by the user using a smartphone. The time did not come in UTC, but the parameters contained coordinates.
Of course, there are ready-made services (for example, The Google Time Zone), but they are all paid or very limited in functionality. So I decided to write my own service.

Service should be as simple as possible. On it we need to do only one request of a type.
http://host/timezone/name?lat=55.2341&lng=43.23352 

Where lat is latitude and lng is longitude.

We are setting up a database

We will use PostgreSQL as the database . We will also need the Postgis extension, specially sharpened for working with geographic objects.
')
We assume that PostgreSQL is already installed. If not, there are many guides and tutorials on the Internet, how to do this. The installation process for Postgis should also be easy - the official site has detailed instructions for most popular operating systems.

After installing all the necessary, create a new database, which we will use to determine the time zone. As an example, I will write “tz_service”:
 CREATE DATABASE tz_service 

We include Postgis in our database:
 CREATE EXTENSION postgis; CREATE EXTENSION postgis_topology; CREATE EXTENSION fuzzystrmatch; CREATE EXTENSION postgis_tiger_geocoder; 

Now we need the shapefile of all time zones from efele.net . Download tz_world.zip. The archive file is tz_world.shp. Shape files contain a vector representation of geographic data. But we need to convert it to a SQL dump and roll it into our tz_service database:
 $ /usr/lib/postgresql/9.1/bin/shp2pgsql -D tz_world.shp > dump.sql $ psql -d tz_service -f dump.sql 

Done! Check job request:
 SELECT tzid FROM tz_world WHERE ST_Contains(the_geom, ST_MakePoint(-122.420706, 37.776685)); 

It should get something like this:
 tzid --------------------- America/Los_Angeles (1 ROW) 

Writing Sevris in Ruby

We will use the Grape framework as the framework for the service. It is great for quickly writing REST-like server applications.
First, create a Gemfile and write down the gems we need:
 source "https://rubygems.org" gem 'rake' gem 'activerecord' gem 'pg' gem 'grape' group :development, :test do gem 'shotgun' gem 'byebug' gem 'pry' gem 'pry-byebug' gem 'rspec' end 

What is in the development group and test is necessary only for development and will not be used in production mode. And you need not so much to develop:
- shotgun in order not to restart the server every time, after the next code change
- buebug and pry for debugging
- rspec for tests

Install all gems with dependencies:
 $ bundle install 

The project tree should look like this:

image

Let's go in order. Let's start with configs.

The config / database.yml will contain information for connecting to the database:
 development: &config adapter: postgresql host: localhost username: user password: password database: tz_service encoding: utf8 test: <<: *config poduction: <<: *config 

Next we set the database configuration class config / configuration.rb for parsing the yaml file:
 class Configuration DB_CONFIG = YAML.load_file(File.expand_path('../database.yml', __FILE__))[ENV['RACK_ENV']] class << self def adapter DB_CONFIG['adapter'] end def host DB_CONFIG['host'] end def username DB_CONFIG['username'] end def password DB_CONFIG['password'] end def database DB_CONFIG['database'] end def encoding DB_CONFIG['encoding'] end end end 

App / environment.rb will contain the environment settings:
 require 'bundler' Bundler.require(:default) $: << File.expand_path('../', __FILE__) $: << File.expand_path('../../', __FILE__) $: << File.expand_path('../../config', __FILE__) $: << File.expand_path('../services', __FILE__) ENV['RACK_ENV'] ||= 'development' require 'grape' require 'json' require 'pry' require 'active_record' require 'timezone_name_service' require 'configuration' require 'application' require 'time_zone_api' 

In app / application.rb, let's write the activerecord settings for the connection with the database:
 ActiveRecord::Base.establish_connection( adapter: Configuration.adapter, host: Configuration.host, database: Configuration.database, username: Configuration.username, password: Configuration.password, encoding: Configuration.encoding ) 

The basis for the service is ready, you only need to write one class of the service itself, which will respond to our request and that's it. Everything? Not! First you need to write tests. Do not forget about TDD.

Create spec / spec_helper.rb and tweak it a bit:
 ENV['RACK_ENV'] ||= 'test' require_relative '../app/environment' require 'rack/test' RSpec.configure do |config| config.treat_symbols_as_metadata_keys_with_true_values = true config.run_all_when_everything_filtered = true config.filter_run :focus config.order = 'random' config.include Rack::Test::Methods def app TimeZoneAPI end end 

In tests, we must describe the behavior of the service And we expect only two things:
1. Adequate response with adequate parameters in the request
2. Error in the absence of parameters
We describe this:
 describe 'API' do let(:params) { { lat: 55.7914056, lng: 49.1120427 } } #    let(:error) { { error: 'lat is missing, lng is missing' } } #      let(:name_response) { { timezone: 'Europe/Moscow' } } #     #  it 'should greet us' do get '/' expect(last_response).to be_ok expect(last_response.body).to eq(%Q{"Welcome to Time Zone API Service"}) end #      describe 'Timezone name' do subject { last_response } #     context 'with wrong params' do before do get '/timezone/name' end its(:status) {should eq 400} its(:body) {should eq error.to_json} end context 'with right params' do before do get '/timezone/name', params end its(:status) {should eq 200} its(:body) {should eq name_response.to_json} end end end 

Running the command:
 $ bundle exec rspec 

No test will pass. Still =) It is necessary to gilt the tests.
We will need to contact the database with a non-standard query. We will do this through the app / services / time_zone_service.rb class:
 class TimezoneNameService def initialize(lat, lng) @lat = lat @lng = lng end def name #"" .    ,        sql = "SELECT tzid FROM tz_world WHERE ST_Contains(geom, ST_MakePoint(#{ActiveRecord::Base.sanitize(@lng)}, #{ActiveRecord::Base.sanitize(@lat)}));" name_hash = ActiveRecord::Base.connection.execute(sql).first name_hash['tzid'] rescue nil end end 

And finally, the main service class app / time_zone_api.rb:
 class TimeZoneAPI < Grape::API format :json default_format :json default_error_formatter :txt desc 'Start page' get '/' do 'Welcome to Time Zone API Service' end namespace 'timezone' do desc 'Time zone name by coordinates' params do requires :lat, type: Float, desc: 'Latitude' requires :lng, type: Float, desc: 'Longitude' end #  get '/name' do name = TimezoneNameService.new(params[:lat], params[:lng]).name { timezone: name } end end end 

That's all! Service is ready. Check his work "live" by running the Grape-application:
 $ bundle exec rackup 

Related Links
Grape Framework
Postgis
Project ID on Github

Source: https://habr.com/ru/post/246077/


All Articles