Rails Postgis Nearest point - ruby-on-rails

Try to calculate nearest points in some distance and one nearest point.
in db/migrate/xxx_create_points.rb":
class CreatePoints < ActiveRecord::Migration
def change
create_table :points do |t|
t.point :location, :geographic => true
t.string :name, :null => false
t.timestamps
end
change_table :points do |t|
t.index :location, :spatial => true
end
end
end
in config/routes.rb:
get 'points/:lat/:lon/:distance', to: 'points#index', :constraints => { :lat => /[^\/]+/, :lon => /[^\/]+/}
in controllers/points_controller.rb:
class PointsController < ApplicationController
def index
#points= Point.all
if params[:distance].present? && params[:lat].present? && params[:lon].present?
#distance = params[:distance].to_i
#latitude = params[:lat].to_f
#longitude = params[:lon].to_f
#points= Point.near(#latitude, #longitude, #distance)
#near = Point.nearest(#latitude, #longitude, 100).first
end
end
In models/point.rb:
class Point < ActiveRecord::Base
set_rgeo_factory_for_column(:location,
RGeo::Geographic.spherical_factory(:srid => 4326))
attr_accessible :location, :name
scope :near, lambda { |latitude, longitude, distance|
where("ST_Distance(location,
"+"'POINT(#{latitude} #{longitude})') < #{distance}")}
scope :nearest, lambda { |latitude, longitude, distance|
where("ST_Distance(location,
"+"'POINT(#{latitude} #{longitude})') < {distance}")
.order("ST_Distance(location, ST_GeographyFromText('POINT(#{latitude} #{longitude})'))").limit(1)}
end
In views/points/index.html.erb:
<script>
$.each(response.data, function(i, point) {
$('#map_canvas').gmap('addMarker', {
'position': new google.maps.LatLng(point.latitude, point.longitude), 'bounds': true });
}
$('#map_canvas').gmap('addShape', 'Circle', {
...
'center': new google.maps.LatLng(<%= "#{#latitude},#{#longitude}" %>),
'radius': 1 });
$('#map_canvas').gmap('addShape', 'Circle', {
...
'center': new google.maps.LatLng(<%= "#{#latitude},#{#longitude}" %>),
'radius': <%= #distance %> });
$('#map_canvas').gmap('addShape', 'Circle', {
...
'center': new google.maps.LatLng(<%= "#{#near.location.x},#{#near.location.y}" %>),
'radius': 2 });
</script>
Result in browser: http://xxx/points?distance=200&lat=55.7632500&lon=52.442000
What am I doing wrong?

PostGIS always uses the coordinate axis order (X Y) or (longitude latitude). I see snippets in your code that has this reversed:
ST_GeographyFromText('POINT(#{latitude} #{longitude})')
This needs to be switched:
ST_MakePoint(#{longitude}, #{latitude})::geography

Google maps uses mercator projections and the difference you see might be caused by the distance calculation differences between spherical projections you use in your model and mercator projections.
Try using RGeo's simple_mercator_factory in your Point class instead of using the spherical_factory.
RGeo's documentation gives you more details about that.
# Many popular visualization technologies, such as Google and Bing
# maps, actually use two coordinate systems. The first is the
# standard WSG84 lat-long system used by the GPS and represented
# by EPSG 4326. Most API calls and input-output in these mapping
# technologies utilize this coordinate system. The second is a
# Mercator projection based on a "sphericalization" of the WGS84
# lat-long system. This projection is the basis of the map's screen
# and tiling coordinates, and has been assigned EPSG 3857.

Related

Same method between model & controller

To practice, I'm trying to create a small web game with Ruby On Rails, however, I need some information in my model and also in my controller.
I explain myself. For example, in my model, I have the following method:
def cost(building)
{
wood: BUILDING[building]['wood'] * (BUILDING[building]['factor'] ** self.level(building)),
stone: BUILDING[building]['stone'] * (BUILDING[building]['factor'] ** self.level(building)),
iron: BUILDING[building]['iron'] * (BUILDING[building]['factor'] ** self.level(building))
}
end
Then in my view I use the following to display the cost of upgrading the building :
<%= #buildings.cost(building[0])[:wood] %>
But I also have an "upgrade" button in my view that allows to upgrade this building, except that on the controller side I also need to get the costs of the building but I'm not sure of the right approach.
[EDIT] :
To give more informations :
#buildings = #buildings = Building.find_by(kingdom_id: current_kingdom.id) #Inside BuildingsController
building[0] It is a string in a yaml that corresponds to the name of a building
level it recovers the current level of the building for the player
EDIT 2 :
models/building.rb :
class Building < ApplicationRecord
def cost(building)
{
wood: BUILDING[building]['wood'] * (BUILDING[building]['factor'] ** self.level(building)),
stone: BUILDING[building]['stone'] * (BUILDING[building]['factor'] ** self.level(building)),
iron: BUILDING[building]['iron'] * (BUILDING[building]['factor'] ** self.level(building))
}
end
def consumption(building)
BUILDING[building]['coal']
end
def time(building)
resources_needed = cost(building)[:wood] + cost(building)[:stone] + cost(building)[:iron]
time = (resources_needed / (2500 * 4 * SERVER['rate']).to_f * 3600).round
if time >= 3600
"#{time / 60 / 60} h #{time % 3600 / 60} min"
elsif time >= 60
"#{time / 60} min #{time % 60 } sec"
else
"#{time} sec"
end
end
def level(building)
self[building.to_sym]
end
def upgrade?(building, kingdom_resources)
cost(building)[:wood] <= kingdom_resources[:wood] &&
cost(building)[:stone] <= kingdom_resources[:stone] &&
cost(building)[:iron] <= kingdom_resources[:iron]
end
end
buildings_controller.rb :
class BuildingsController < ApplicationController
def index
#buildings = Building.find_by(kingdom_id: current_kingdom.id)
#kingdom_resources = kingdom_resources
#kingdom_queue = BuildQueue.where(kingdom_id: current_kingdom.id)
end
def add_to_queue
building = params[:building]
# If we can upgrade
# Add building to the queue
end
private
def cost(building)
{
wood: BUILDING[building]['wood'] * (BUILDING[building]['factor'] ** self.level(building)),
stone: BUILDING[building]['stone'] * (BUILDING[building]['factor'] ** self.level(building)),
iron: BUILDING[building]['iron'] * (BUILDING[building]['factor'] ** self.level(building))
}
end
def building_level(building)
Building.find_by(kingdom_id: current_kingdom.id)[building.to_sym]
end
def time(building)
resources_needed = cost(building)[:wood] + cost(building)[:stone] + cost(building)[:iron]
(resources_needed / (2500 * 4 * SERVER['rate']).to_f * 3600).round
end
def upgrade?(building)
cost(building)[:wood] <= kingdom_resources[:wood] &&
cost(building)[:stone] <= kingdom_resources[:stone] &&
cost(building)[:iron] <= kingdom_resources[:iron]
end
end
method inside app/controllers/application_controller.rb,
To get the current_kingdom :
def current_kingdom
return nil unless current_user
return #_kingdom if #_kingdom
#_kingdom = Kingdom.find_by(user_id: current_user.id)
end
And current_user :
def current_user
return nil if !session[:auth] || !session[:auth]['id']
return #_user if #_user
#_user = User.find_by_id(session[:auth]['id'])
end
And current kingdom_resources :
def kingdom_resources
return #kingdom if #kingdom
#kingdom = {
wood: current_kingdom.wood,
stone: current_kingdom.stone,
iron: current_kingdom.iron,
coal: current_kingdom.coal,
food: current_kingdom.food
}
end
Thank's in advance,
Regards
fA user has a kingdom. A kingdom has buildings. We can set that up simple enough. A kingdom apparently also has build queues and resources.
class User < ApplicationRecord
has_one :kingdom
has_many :buildings, through: :kingdom
end
create_table :kingdoms do |t|
t.belongs_to :user, foreign_key: true
t.name :string, null: false
t.wood :integer, null: false
t.stone :integer, null: false
t.iron :integer, null: false
t.coal :integer, null: false
t.food :integer, null: false
end
class Kingdom < ApplicationRecord
belongs_to :user
has_many :buildings
has_many :build_queues
def resource(material)
self[material]
end
end
class BuildQueue < ApplicationRecord
belongs_to :kingdom
belongs_to :building
end
Now you can directly ask a player for its buildings: current_user.buildings and a kingdom for its build queues: kingdom.build_queues.
If you want to find a building by name: current_user.buildings.find_by(name: building_name).
Your Building model is strange. It seems like a single Building object represents all buildings. And information about the cost of a building is stored in a global.
Instead, the information about each building should instead be stored in a table row.
create_table :buildings do |t|
t.belongs_to :kingdom, foreign_key: true
t.string :name, null: false
t.cost_wood :integer, null: false
t.cost_stone :integer, null: false
t.cost_iron :integer, null: false
t.consumption_coal :integer, null: false
t.cost_factor :float, default: 1, null: false
t.level :integer, default: 1, null: false
end
class Building < ApplicationRecord
belongs_to :kingdom
has_one :player, through :kingdom
MATERIALS = [:wood, :stone, :iron].freeze
private def base_cost(material)
self[:"cost_#{material}"]
end
def cost(material)
base_cost(material) * cost_factor ** level
end
def build_cost
MATERIALS.sum { |m| cost(m) }
end
def build_time
# ActiveSupport::Duration will do the formatting for you.
ActiveSupport::Duration.build(
(build_cost / (2500 * 4 * SERVER['rate']).to_f * 3600).round
)
end
end
Now the BuildingsController can use what's been set up in the models.
class BuildingsController < ApplicationController
def index
# A user already has associations to its kingdom and buildings
# This is so simple there's no need for a current_kingdom in
# the ApplicationController
#kingdom = current_user.kingdom
#buildings = current_user.buildings
end
def show
if params[:id].present?
#building = current_user.buildings.find_by(id: params[:id])
else
#building = current_user.buildings.find_by(name: params[:name])
end
end
end
If you want to take action that involves multiple models, make a little object to do it. Like queuing a building upgrade.
# BuildingUpgradeQueuer.new(
# building: building,
# kingdom: kingdom
# ).queue_upgrade
class BuildingUpgradeQueuer
include ActiveModel::Model
MATERIALS = [:wood, :stone, :iron].freeze
attr_accessor :buiding, :kingdom
def queue_upgrade
return unless upgrade?
kingdom.build_queues.create!(building: building)
end
def upgrade?
MATERIALS.all? { |material|
building.cost(material) <= kingdom.resources(material)
}
end
end
You could do this in the Building or Kingdom object, but if you do that your models get complex and fat.

rails Calculation (price * quantity)

Hi Im creating an ec site in my rails.
My migration:
(Item) has :name and :price.
(Basket_Item) has :item_id(fk), :basket_id(fk) and :quantity.
The system User will add some items to their basket.
So Basket_items is JOIN Table between (Item) and (Basket)
see like below.
What I want to do:
Get a price of Item and get a quantity from Basket_Items which is selected by user. Then I want to create #total_price = item_price * item_quantity.
Can anyone help me to create the #total_price please.
This is my a try code but it doesn't work on rails console.
Basket_items
class CreateBasketItems < ActiveRecord::Migration[5.2]
def change
create_table :basket_items do |t|
t.references :basket, index: true, null: false, foreign_key: true
t.references :item, index: true, null: false, foreign_key: true
t.integer :quantity, null: false, default: 1
t.timestamps
end
end
end
///
Items
class CreateItems < ActiveRecord::Migration[5.2]
def change
create_table :items do |t|
t.references :admin, index: true, null: false, foreign_key: true
t.string :name, null: false, index: true
t.integer :price, null: false
t.text :message
t.string :category, index: true
t.string :img
t.string :Video_url
t.text :discription
t.timestamps
end
end
end
///
This is my try a code but it doesn't work on rails console.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price)
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity)
def self.total(items_price, items_quantity)
sum(items_price * items_quantity)
end
#total_price = basket.total(items_price, item_quantity)
There are a few issues with your code:
You are trying to call a class method on an instance of the class. That's not gonna work, second you are passing in arrays into the calculation.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price) # => Array of prices from the items in the basket
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity) # => Array of quantities from the items in the basket
def self.total(items_price, items_quantity)
sum(items_price * items_quantity) # => So this line will do: sum(['12,95', '9.99'] * [1, 3])
end
#total_price = basket.total(items_price, item_quantity)
As you can see, that ain't gonna work. First you need to change the method and remove the self.
def total(items_price, items_quantity)
# ...
end
Now you can call the total method on a basket object: basket.total(items_price, items_quantity)
And inside the total method you need to loop through each items to do the calculation and add all the results.
def total(items_price, items_quantity)
total_price = 0
items_price.each_with_index do |price, index|
total_price += price * items_quantity[index]
end
total_price
end
But this solution could also fail, because you don't know sure that the order in the items_price is matching with the order of items_quantity. So a better approach would be to do the calculation for each basket_item seperate.
# Basket model
def total
total_price = 0
basket_items.each do |basket_item|
total_price += basket_item.total_price
end
total_price
end
# BasketItem model
def total_price
quantity * item.price
end
Now you can call it like this: basket.total

Geocoding multiple addresses in one model

I am trying to geocode 2 addresses in a model using geocoder and I can't get gem to work as I want to. Here is the code that I am applying to my model:
class Sender < ActiveRecord::Base
validates_presence_of :source_address
validates_presence_of :destination_address
geocoded_by :source_address, :latitude => :latitude1, :longitude => :longitude1
geocoded_by :destination_address, :latitude2 => :latitude2, :longitude2 => :longitude2
def update_coordinates
geocode
[latitude1, longitude1, latitude2, longitude2]
end
after_validation :geocode
Here is code for views/senders/show.html.erb:
<%= #sender.latitude1 %>
<%= #sender.longitude1 %>
<%= #sender.latitude2 %>
<%= #sender.longitude2 %>
Result : 35.6894875 139.6917064 - Isn't it supposed to send me back 2 address information?
Here is my js:
<script type="text/javascript">
function initialize() {
var source = new google.maps.LatLng(<%= #sender.latitude1 %>, <%= #sender.longitude1 %>);
var dest = new google.maps.LatLng(<%= #sender.latitude2 %>, <%= #sender.longitude2 %>);
var mapOptions = {
center: source,
zoom: 8
}
var mapOptions2 = {
center: dest,
zoom: 8
}
var map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
var map2 = new google.maps.Map(document.getElementById('map_canvas2'), mapOptions2);
var marker = new google.maps.Marker({
position:source,
map: map
});
var marker2 = new google.maps.Marker({
position:dest,
map: map2
});
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
The problem and solution are mentioned here.
Add the following before_save and corresponding method to your model to solve the problem. Don't forget to repeat the part of code for the second location (maybe destination):
before_save :geocode_endpoints
private
#To enable Geocoder to works with multiple locations
def geocode_endpoints
if from_changed?
geocoded = Geocoder.search(loc1).first
if geocoded
self.latitude = geocoded.latitude
self.longitude = geocoded.longitude
end
end
# Repeat for destination
if to_changed?
geocoded = Geocoder.search(loc2).first
if geocoded
self.latitude2 = geocoded.latitude
self.longitude2 = geocoded.longitude
end
end
end
Rewrite
def function
...
end
as:
def update_coordinates
geocode
[latitude, longitude, latitude2, longitude2]
end
And also:
geocoded_by :destination_address, :latitude => :latitude2, :longitude => :longitude2
You also don't need :latitude => :lat, :longitude => :lon here:
geocoded_by :source_address, ...
And finally, coordinates are fetched automatically after record is validated. So you could do without update_coordinates (or function, in your version) and arrange the view for show action like this:
<%= #sender.latitude %>
<%= #sender.longitude %>
<%= #sender.latitude2 %>
<%= #sender.longitude2 %>

gmaps4rails1.5.1-Geocoding not working

Location added to table. But missing latitude/longitude. So no marker added to the map.
class Location < ActiveRecord::Base
validates :priority,:addr,:presence=>true
validates :priority,:numericality=>{:greater_than_or_equal_to =>1,:less_than_or_equal_to =>6}
default_scope :order=>'priority'
acts_as_gmappable :lat=>'lat',:lng=>'lng'
def gmaps4rails_address
self.addr
end
def gmaps4rails_infowindow
"<h4>Target priority: #{priority}</h4>" << "<h4>Address: #{addr}</h4>" << "<h4>Latitude: #{lat}</h4>" << "<h4>Longitude: #{lng}</h4>"
end
def gmaps4rails_marker_picture
{
"picture" => "/images/#{priority%7}.jpg",
"width" => "30",
"height" => "30"
}
end
end
Simply set gmaps to false to trigger geocoding (as the doc tells)

Displaying a polygon with gmaps4rails

Ruby noob here. Trying to display a list of points as a polygon on a google map using the gmaps4rails gem (awesome gem by the way). Any suggestions based on code sample below would be much appreciated! I can see the outline for the map, but no map and no polygon. Update: this code has been updated and the problem is solved.
Class Schoolpoint is a list of lat/long pairs that belong to School
In my controller:
#polyjson = []
schoolpoints = []
Schoolpoint.where(:school_id => params[:id]).each do |point|
schoolpoints << { :lng => point.longitude, :lat => point.latitude}
end
#polyjson = [schoolpoints]
#polyjson = #polyjson.to_json
Then in the view:
<%= gmaps({"polygons" => { "data" => #polyjson }})
in Schoolpoint model:
class Point < ActiveRecord::Base
belongs_to :place
acts_as_gmappable :process_geocoding => false
def gmaps4rails_address
"#{longitude}, #{latitude}"
end
end
Update: This error now longer exists, but I've left it in case its helpful to someone dealing with similar problem. Finally, here is the resulting js with an uncaught Syntaxerror => unexpected token.
Gmaps.map = new Gmaps4RailsGoogle();
Gmaps.load_map = function() {
Gmaps.map.initialize();
Gmaps.map.polygons = [[{"lng"=>-80.190262, "lat"=>25.774252, "strokeColor"=>"#FF0000", "strokeOpacity"=>0.3, "strokeWeight"=>1, "fillColor"=>"#FF0000", "fillOpacity"=>0.7}, {"lng"=>-87.6245284080505, "lat"=>41.8868315803506}, {"lng"=>-87.6241636276245, "lat"=>41.8674515900783}, {"lng"=>-87.6203870773315, "lat"=>41.8674835487326}, {"lng"=>-87.6167392730712, "lat"=>41.8579591627635}, {"lng"=>-87.6348495483398, "lat"=>41.8577034549953}, {"lng"=>-87.6342701911926, "lat"=>41.8588701133785}, {"lng"=>-87.6341199874878, "lat"=>41.858946025344}, {"lng"=>-87.6341146230697, "lat"=>41.8590858629394}, {"lng"=>-87.6341199874878, "lat"=>41.8600767034266}, {"lng"=>-87.6342219114303, "lat"=>41.8612433185139}, {"lng"=>-87.634157538414, "lat"=>41.8613112372298}, {"lng"=>-87.6342540979385, "lat"=>41.8621502271823}, {"lng"=>-87.6341950893402, "lat"=>41.8622580965204}, {"lng"=>-87.6342433691024, "lat"=>41.8626336402037}, {"lng"=>-87.6341092586517, "lat"=>41.8630930789441}, {"lng"=>-87.6342326402664, "lat"=>41.8631010691539}, {"lng"=>-87.6342862844467, "lat"=>41.8651984646832}, {"lng"=>-87.6342165470123, "lat"=>41.865314318812}, {"lng"=>-87.6342540979385, "lat"=>41.865929540668}, {"lng"=>-87.6343238353729, "lat"=>41.8661652409794}, {"lng"=>-87.6343667507171, "lat"=>41.8664728485533}, {"lng"=>-87.6342701911926, "lat"=>41.866564731048}, {"lng"=>-87.6343882083892, "lat"=>41.8673317449823}, {"lng"=>-87.6344525814056, "lat"=>41.8680388278011}, {"lng"=>-87.6346457004547, "lat"=>41.8691693450993}, {"lng"=>-87.6346671581268, "lat"=>41.8696886572982}, {"lng"=>-87.6345813274383, "lat"=>41.8698804022745}, {"lng"=>-87.6347583532333, "lat"=>41.869992253245}, {"lng"=>-87.634892463684, "lat"=>41.8706873227465}, {"lng"=>-87.6353269815445, "lat"=>41.8726167002032}, {"lng"=>-87.6352626085281, "lat"=>41.8728443868687}, {"lng"=>-87.6354557275772, "lat"=>41.8730081609862}, {"lng"=>-87.6353698968887, "lat"=>41.8732797854267}, {"lng"=>-87.6356971263885, "lat"=>41.8740227522642}, {"lng"=>-87.6356971263885, "lat"=>41.8746458790817}, {"lng"=>-87.6359224319458, "lat"=>41.87509724279}, {"lng"=>-87.6361316442489, "lat"=>41.8754088017203}, {"lng"=>-87.6364105939865, "lat"=>41.8754727110567}, {"lng"=>-87.6364642381668, "lat"=>41.8757642965932}, {"lng"=>-87.6371240615844, "lat"=>41.876678987795}, {"lng"=>-87.637939453125, "lat"=>41.8801059676767}, {"lng"=>-87.6379930973053, "lat"=>41.8806172030015}, {"lng"=>-87.6378536224365, "lat"=>41.8829017358812}, {"lng"=>-87.6375961303711, "lat"=>41.8844593251054}, {"lng"=>-87.6372849941253, "lat"=>41.8857213439117}, {"lng"=>-87.6371347904205, "lat"=>41.8860408383893}, {"lng"=>-87.6355576515197, "lat"=>41.8870552227663}, {"lng"=>-87.6282513141632, "lat"=>41.8870951588295}, {"lng"=>-87.6281654834747, "lat"=>41.8868076186168}]];
153:2439 Uncaught SyntaxError: Unexpected token >
Gmaps.map.create_polygons();
Gmaps.map.adjustMapToBounds();
Gmaps.map.callback();
};
window.onload = function() { Gmaps.loadMaps(); };
I passed their base tutorial (from screen cast) and markers works fine. But I had problem with polylines (as you with polygons). At the end I resolved my problem (it can help you with polygons).
So, view is the same as they gave :
<%= gmaps({
"polylines" => { "data" => #bemap_polylines }
})
%>
The model is also the same as their.
class Character < ActiveRecord::Base
belongs_to :bemap
acts_as_gmappable
def gmaps4rails_address
#describe how to retrieve the address from your model, if you use directly a db column, you can dry your code, see wiki
address
logger.info address
end
end
So, the main problem was into controller. Here is mine :
def show
#bemap = Bemap.find(params[:id])
#bemap_polylines = []
#bemap_characters = []
#bemap.characters.each do |v|
#bemap_characters << { :lng => v[:longitude], :lat => v[:latitude]}
end
#bemap_polylines << #bemap_characters
#bemap_polylines = #bemap_polylines.to_json
respond_to do |format|
format.html # show.html.erb
format.json { render json: #bemap }
end
end
Gmaps4rails doesn't provide any builtin method to create the js for polygons.
The one you use is malformed, see doc: https://github.com/apneadiving/Google-Maps-for-Rails/wiki/Polygons.

Resources