3

How can I check if my lat/long is in the city limits or example, Greater London is enclosed by:

[bbox=-0.489,51.28,0.236,51.686]

Source :

http://wiki.openstreetmap.org/wiki/Bounding_Box

How can I check if a location (lat/lon):

51.55238,0.047032

Is there a gem already for this? Or what would be the best way to do this?

Bounty update:

I have a working solution but I feel its not the right one, I'm using geocoder gem, and my user has geocoded_by, and lat/long attributes. So here is how I do it :

def self.user_from_dublin(user_id)
  near([53.349937,-6.261917], 15).pluck(:id).include?(user_id)
end

So this loads all users from Dublin and compares the id of the user to those users in Dublin. If the id is found the user is considered to be from Dublin.

I don't think this will work well when there is more users.

Update

I'm using postgres database

Gandalf StormCrow
  • 25,788
  • 70
  • 174
  • 263
  • if box.lowx <= x <= box.highx and box.lowy <= y <= box.highy ? – David Jan 27 '14 at 20:08
  • @David thanks for your response. Can you please write an answer? and/or explain box.lowx and box.lowy from the bbox values? – Gandalf StormCrow Jan 27 '14 at 20:13
  • @GandalfStormCrow http://www.rubygeocoder.com/ and you can use mongodb it supports querying based on geocodes – bjhaid Jan 27 '14 at 20:22
  • @bjhaid can I do it without mongodb, do I really need mongo db? I already got lat/lon in my postgres table and I all I need to check is are the users lat/lon in the bounding box – Gandalf StormCrow Jan 27 '14 at 20:24
  • @GandalfStormCrow I gave a link to the homepage of the `geocoder gem` and suggested mongodb as an alternative – bjhaid Jan 27 '14 at 20:25
  • How much do you care about accuracy? What if a geolocation is a little outside the city limit, how much does this matter to you? (The reason I'm asking is because you'll be facing performance issues as you get more cities/users) – Abdo Jan 31 '14 at 00:48
  • http://www.rubygeocoder.com/ – Aleksei Matiushkin Jan 31 '14 at 06:58

9 Answers9

3

Maybe this is an overkill, but if you are using postgres you can install postgis extension to manage spatial data. Then in a irb you can do something like this:

result = ActiveRecord::Base.connection.execute('SELECT
ST_Contains(ST_SetSRID(ST_MakeBox2D(ST_Point(-0.489, 51.28), ST_Point(0.236, 51.686)), 4326),
ST_SetSRID(ST_Point(-0.1265, 51.483), 4326))')

The query is checking if the point is inside the given bbox using the ST_contains function

This will return:

=> #<PG::Result:0x007fa517fcbe08 @connection=#<PG::Connection:0x007fa5167f8970 @socket_io=nil, @notice_receiver=nil, @notice_processor=nil>>

Then you can do:

result.first

This will return:

{"st_contains"=>"t"}

With this point -0.7265, 44.483 (a point outside the bbox) the result will be:

{"st_contains"=>"f"}

If you don't want to use raw sql you can use gems to manage spatial data. A very good one is: rgeo. I recommend to read the creator's blog

Using rgeo you can define attributes for your models with "geotypes" like points, polygons, etc. and then use functions like contains?

I leave you with a gist with very basic instructions to install postgis with ubuntu.

Leantraxxx
  • 4,506
  • 3
  • 38
  • 56
3

Can you use raw sql? The <@ geometric operator will check if the point is contained in the box

select '51.55238,0.047032'::point <@ '-0.489,51.28,0.236,51.686'::box;
 ?column? 
----------
 f
Clodoaldo Neto
  • 118,695
  • 26
  • 233
  • 260
3

You don't need anything special for this.

Bounding Box is a rectangle:

bbox = left,bottom,right,top

  +-----------(10,0)
  |              |
  |              |
  |              |
  |              |
  |              |
(0,10)-----------+

The weird (left-bottom) to (top-right) numbering is because, Longitude is measured left-to-right (to simplify, -180° is left of the globe, +180° is right of the globe), whereas Latitude is measured bottom-to-top (to simplify, -90° is bottom of the globe, +90° is top of the globe).

So as per the example,

(0,10)   = left, bottom = min longitude, min latitude
(10,10)  = right, top   = max longitude, max latitude

Now you want to know whether a point (5,5) falls inside it.

  +-----------(10,10)
  |              |
  |              |
  |     (5,5)    |
  |              |
  |              |
(0,0)------------+

So all you have to do is just create a table for cities in Postgres (or any database for that matter):

CREATE TABLE cities(
  city_name      VARCHAR(xxx),
  min_longitude  DECIMAL,
  min_latitude   DECIMAL,
  max_longitude  DECIMAL,
  max_latitude   DECIMAL
);

Now suppose you want to find out which city falls under a position latitude=5, longitude=6, then just use the following query:

SELECT city_name 
FROM cities 
WHERE (min_latitude  >= 6 AND max_latitude  <= 6)
AND   (min_longitude >= 5 AND max_longitude <= 5)

Substitute 5 and 6 with the user's latitude/longitude, and you will get the city where the user falls under.

EDIT: Small mix-up, minor formatting

Subhas
  • 14,290
  • 1
  • 29
  • 37
  • I think you are missing use cases which cross the equator or prime meridian. This would be a good reason to use an established gem instead. – eabraham Feb 05 '14 at 22:24
  • 1
    I don't see why there would be a problem on the equator etc because the lat/longt simply crosses 0 on a continuous function. It would fail on the poles though. It seems like a decent simple solution as long as you're not doing something like boxing in giant tracts of land in N. Greenland. – seand Feb 06 '14 at 07:13
  • 1
    Actually you "will" have discontinuity on the opposite side of the prime meridian. Cross over the line and longitude 180 becomes -180. – seand Mar 01 '14 at 03:23
2

If its a boolean why not use where to limit the results to only your user. This way it will just involve one SQL query that will either be empty or have a size of 1 (your user being the only record)

def self.user_from_dublin(user_id)
  !(near([53.349937,-6.261917], 15).where(id: user_id).empty?)
end

empty? will be true when your user is not within your boundary, so the ! at the beginning will negate this and your function will return false when the user is not within the boundary.

Logan Serman
  • 29,447
  • 27
  • 102
  • 141
2

You can use this Gem https://github.com/square/border_patrol

First you need a small KLM with the limits of the city you want

rhernando
  • 1,051
  • 7
  • 18
1

A KISS solution might be helpful since you have lat and lon attributes to the user model.

If you have London's box you could form an activerecord scope which would implement

LONDON_BOX = [-0.489,51.28,0.236,51.686]
scope :inside_london -> {
  where(lat: LONDON_BOX[1]..LONDON_BOX[3]).
  where(lon: LONDON_BOX[0]..LONDON_BOX[2])
}

thus enabling you to use
User.inside_london

Keep in mind that this type of double range query will not perform that good, it cannot use a proper index in MySQL. For that @david has provided a useful comment, the use of mongodb and its geospatial abilities will help. (But a DB change is a serious decision)

xlembouras
  • 8,215
  • 4
  • 33
  • 42
1

BoundBox is rectangle and not an exact city boundary. And it's not efficient to calculate city boundary in each query. I think you can save city relation in callback before save record using geocoder. Please share your solution if you have better one.

Chen
  • 578
  • 2
  • 9
1

I've made a few assumptions, let me know if they are true:

Assumption #1: This lat/long represents where a user lives and will be updated infrequently when a user physically moves his home.

Assumption #2: This lat/long does not represent a users current position and will be updated constantly

Assumption #3: You have bounding boxes for all of the cities you wish to include in the system

Given these assumptions, I think the best way to handle this is to place each person into "City" buckets. Create a city model with a many to many relationship with user:

class City < ActiveRecord::Base
  has_many :user_city
  has_many :users, :through => :user_city

  attr_accessible :name, :bounding_box #bounding box is an array with 4 values which represent the corners of the city limits.
end

class User < ActiveRecord::Base
  after_validation :geocode 
  after_create :assign_city

  has_many :user_city
  has_many :city, :through => :user_city

  attr_accessible :email, :created_at, :password, :updated_at, :latitude, :longitude

  def assign_city
    City.all.each do |city|
      inCity = self.within_bounding_box(city.bounding_box)
      if inCity
       UserCity.create(:user=>self,:city=>city)
       break
      end
    end
  end

end

class UserCity < ActiveRecord::Base
  belongs_to :user
  belongs_to :city

  attr_accessible :user, :city
end

Now you can query users in a city with:

User.joins(:city).where({:cities=>{:name=>"London"}})
eabraham
  • 4,094
  • 1
  • 23
  • 29
1

You don't exactly say... but if what you want is the name of the nearest city to a Lat/Lon coordinate anywhere, then you could use google maps Geocoder services - set up a GeocoderRequest with a LatLng to search and issue a request.

TonyWilk
  • 1,447
  • 10
  • 13