(Ruby on Rails 6 only API, Postgres, Minitest)
Hi everybody,
Sometimes, we need specific features in our application. One of them may be “How to find Coordinates in Polygon” :)
We suppose, we have some areas in our application. We define these areas by using Google Maps and every area has some specific attributes that are name , price, and coordinates. For example, we need information like this, when we click on a point on the map, what are the properties of this point? How to find if the dots are between the corresponding coordinates?
GEMS
If we are using Ruby on Rails, we think firstly that “is there any Gem about finding these points? Yes, there are some Gems. Like :
https://github.com/geokit/geokit
https://github.com/apneadiving/Google-Maps-for-Rails
https://github.com/alexreisner/geocoder
But these gems little huge :) Actually, we don’t need to use completely a gem. Because we just find a point in a polygon, technically. There is a cost of a gem for our application. That's why I select a module from Geokit gem :
https://github.com/geokit/geokit/blob/master/lib/geokit/polygon.rb
https://github.com/geokit/geokit/blob/master/lib/geokit/lat_lng.rb
I think these files are enough for this feature.
LETS DEVELOP
Today, I will develop in Ruby on Rails 6 only API. DB is Postgres.
Of course, we will proceed with the TDD method by using Minitest.
For fast development, I prefer “scaffold”
rails g scaffold geofence area_name:string — apiRunning via Spring preloader in process 37838
invoke active_record
create db/migrate/20200126041210_create_geofences.rb
create app/models/geofence.rb
invoke test_unit
create test/models/geofence_test.rb
create test/fixtures/geofences.yml
invoke resource_route
route resources :geofences
invoke scaffold_controller
create app/controllers/geofences_controller.rb
invoke test_unit
create test/controllers/geofences_controller_test.rb
We created geofence.rb model and just one attribute area_name and other processes.
We need to add coordinates array to the geofence model.
“db/migrate/20200126041210_create_geofences.rb”
class CreateGeofences < ActiveRecord::Migration[6.0]
def change
create_table :geofences do |t|
t.float :coordinates, :array => true
t.string :area_name
t.timestamps
end
end
end
and “db/schema “ :
ActiveRecord::Schema.define(version: 2020_01_23_041217) do# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"create_table "geofences", force: :cascade do |t|
t.float "coordinates", array: true
t.string "area_name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
endend
We need to check tests :
rails test test/controllers/geofences_controller_test.rb
Little magical. We didn't do anything but works all tests :)
Finished in 0.554706s, 12.6193 runs/s, 19.8303 assertions/s.
7 runs, 11 assertions, 0 failures, 0 errors, 0 skips
We define a new method which name is “find_by coordinate” in Geofence Controller.rb to find about we wanting coordinates.
we can add firstly in routes file :
Rails.application.routes.draw do
resources :geofences
post 'find_by_coordinate', to: 'geofences#find_by_coordinate#'
end
And Geofence Controller.rb :
def find_by_coordinate
target_coords = params[:target_coordinates]
data = @geofence.find_target_coordinates target_coords
if data.present?
render json: @geofence
else
render json: @geofence.errors, status: :unprocessable_entity
end
end
I am seeing a method in geofence.rb class :
@geofence.find_target_coordinates target_coords
geofence.rb :
require "#{Rails.root}/lib/geokit/lat_lng.rb"
require "#{Rails.root}/lib/geokit/polygon.rb"class Geofence < ApplicationRecordattr_accessor :lat, :lngdef find_target_coordinates target_coordinates
@lat = target_coordinates[:lat]
@lng = target_coordinates[:lng]
if contains_point?
puts "Geofence Model - Success - We found geofence by lat : #{lat} / lng : #{@lng} in Geofence [ ID : #{self.id} / coordinated : #{self.coordinates} ] "
self
else
puts "Geofence Model - Warning - We did not found geofence by lat : #{lat} / lng : #{@lng} in Geofence [ ID : #{self.id} / coordinated : #{self.coordinates} ] "
errors.add(:base, "We did not found geofence.")
nil
end
endprivatedef contains_point?
points = []
self.coordinates.each do |coord|
points << Geokit::LatLng.new(coord[0], coord[1])
end
polygon = Geokit::Polygon.new(points)
point = Point.new(@lat, @lng)
polygon.contains? point
endclass Point
attr_accessor :lat, :lng
def initialize(lat, lng)
self.lat = lat.to_f
self.lng = lng.to_f
end
endend
As seen, We have 2 requires files. I copy and paste the Geokit module in my application lib folders. And I just use to find the required area in a polygon.
Firstly, I add the geofence model coordinates to LatLng.rb class.
points << Geokit::LatLng.new(coord[0], coord[1])
Then, I created “Point.rb” class for target coordinates. We need this class, because the Polygon module wants Point objects.
point = Point.new(@lat, @lng)class Point
attr_accessor :lat, :lng
def initialize(lat, lng)
self.lat = lat.to_f
self.lng = lng.to_f
end
end
TESTING
We need more 2 tests for testing finding the right and wrong coordinates.
We can add a test in test/controllers/geofences_controller_test.rb
test "should find_by_coordinate_url geofence" do
puts "Testing should find geofence"
data = {}
data[:id] = @geofence.id
data[:target_coordinates] = {"lat": 37.7615389, "lng": -122.4144601}
post find_by_coordinate_url, params: data, as: :json
assert_response 200
res = JSON.parse(response.body)
assert_equal @geofence.area_name, res['area_name']
end
This method has “target coordinates” that is a customer click these points for getting specific area properties.
We got a geofence model :
setup do
@geofence = geofences(:one) #getting geofence.yml a record
end
from “/fixtures/geofence.yml”
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.htmlone:
coordinates: [[37.796285118327, -122.557322342739], [37.8277482679855, -122.380854447231], [37.7170254390543, -122.333475907192], [37.6876887080233, -122.552515824184]]
area_name: downtowntwo:
coordinates: [[37.7116950858012, -122.329346944102], [37.6812693981973, -122.551820088633], [37.5539984566077, -122.565552998789], [37.3828740274489, -122.46804933668], [37.4722999873452, -121.994263936289]]
area_name: san mateo
runnings test :
➜ rails test test/controllers/geofences_controller_test.rb
Running via Spring preloader in process 38195
Run options: --seed 20388# Running:Testing index
Testing show
Testing update
..Testing create
Testing destroy
...Testing should find geofence
Geofence Model - Success - We found geofence by lat : 37.7615389 / lng : -122.4144601 in Geofence [ ID : 980190962 / coordinated : [[37.796285118327, -122.557322342739], [37.8277482679855, -122.380854447231], [37.7170254390543, -122.333475907192], [37.6876887080233, -122.552515824184]] ]
.Testing should find geofence
Finished in 0.554706s, 12.6193 runs/s, 19.8303 assertions/s.
7 runs, 11 assertions, 0 failures, 0 errors, 0 skips
and what will happen with the wrong data :
test "should not find_by_coordinate_url geofence" do
puts "Testing should find geofence"
data = {}
data[:id] = @geofence.id
data[:target_coordinates] = {"lat": 39.7615389, "lng": -122.4144601}
post find_by_coordinate_url, params: data, as: :json
assert_response 422
end
result :
➜ rails test test/controllers/geofences_controller_test.rb
Running via Spring preloader in process 38195
Run options: --seed 20388# Running:Testing index
Testing show
Testing update
..Testing create
Testing destroy
...Testing should find geofence
Geofence Model - Warning - We did not found geofence by lat : 39.7615389 / lng : -122.4144601 in Geofence [ ID : 980190962 / coordinated : [[37.796285118327, -122.557322342739], [37.8277482679855, -122.380854447231], [37.7170254390543, -122.333475907192], [37.6876887080233, -122.552515824184]] ]
.Finished in 0.554706s, 12.6193 runs/s, 19.8303 assertions/s.
7 runs, 11 assertions, 0 failures, 0 errors, 0 skips
works!
You can reach this source my GitHub repository :
https://github.com/muratatak77/api_find_cooords_in_polygon
Thanks for reading.