I have been testing my geolocation query for some time now and I haven't found any issues with it until now.
I am trying to search for all cities within a given radius, often times I'm searching for cities surrounding a city using that city's coords, but recently I tried searching around a city and found that the city itself was not returned.
I have these cities as an excerpt in my database:
city latitude longitude
Saint-Mathieu 45.316708 -73.516253
Saint-Édouard 45.233374 -73.516254
Saint-Michel 45.233374 -73.566256
Saint-Rémi 45.266708 -73.616257
But when I run my query around the city of Saint-Rémi, with the following query...
SELECT tblcity.city, tblcity.latitude, tblcity.longitude,
truncate((degrees(acos( sin(radians(tblcity.latitude))
* sin(radians(45.266708))
+ cos(radians(tblcity.latitude))
* cos(radians(45.266708))
* cos(radians(tblcity.longitude - -73.616257) ) ) )
* 69.09*1.6),1) as distance
FROM tblcity HAVING distance < 10 ORDER BY distance desc
I get these results:
city latitude longitude distance
Saint-Mathieu 45.316708 -73.516253 9.5
Saint-Édouard 45.233374 -73.516254 8.6
Saint-Michel 45.233374 -73.566256 5.3
The town of Saint-Rémi is missing from the search.
So I tried a modified query hoping to get a better result:
SELECT tblcity.city, tblcity.latitude, tblcity.longitude,
truncate(( 6371 * acos( cos( radians( 45.266708 ) )
* cos( radians( tblcity.latitude ) )
* cos( radians( tblcity.longitude )
- radians( -73.616257 ) )
+ sin( radians( 45.266708 ) )
* sin( radians( tblcity.latitude ) ) ) ),1) AS distance
FROM tblcity HAVING distance < 10 ORDER BY distance desc
But I get the same result...
However, if I modify Saint-Rémi's coords slighly by changing the last digit of the lat or long by 1, both queries will return Saint-Rémi. Also, if I center the query on any of the other cities above, the searched city is returned in the results.
Can anyone shed some light on what may be causing my queries above to not display the searched city of Saint-Rémi? I have added a sample of the table (with extra fields removed) below.
I'm using MySQL 5.0.45, thanks in advance.
CREATE TABLE `tblcity` (
`IDCity` int(1) NOT NULL auto_increment,
`City` varchar(155) NOT NULL default '',
`Latitude` decimal(9,6) NOT NULL default '0.000000',
`Longitude` decimal(9,6) NOT NULL default '0.000000',
PRIMARY KEY (`IDCity`)
) ENGINE=MyISAM AUTO_INCREMENT=52743 DEFAULT CHARSET=latin1 AUTO_INCREMENT=52743;
INSERT INTO `tblcity` (`city`, `latitude`, `longitude`) VALUES
('Saint-Mathieu', 45.316708, -73.516253),
('Saint-Édouard', 45.233374, -73.516254),
('Saint-Michel', 45.233374, -73.566256),
('Saint-Rémi', 45.266708, -73.616257);
In your first query, I believe you've inverted the longitudes in the subtraction. The Spherical Law of Cosines is:
d = acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(long2−long1))*R
If lat1 is substituted with tblcity.latitude, long1 must be substituted with tblcity.longitude. I think you've accidentally substituted long2 in your query. Does this one work better?
SELECT tblcity.city, tblcity.latitude, tblcity.longitude,
truncate((degrees(acos( sin(radians(tblcity.latitude))
* sin(radians(45.266708))
+ cos(radians(tblcity.latitude))
* cos(radians(45.266708))
* cos(radians(-73.616257 - tblcity.longitude) ) ) )
* 69.09*1.6),1) as distance
FROM tblcity HAVING distance < 10 ORDER BY distance desc
I haven't looked into your second query yet, but hopefully that helps.
You are using the "spherical law of cosines" formula, which is susceptable to rounding error at small distances (like zero!) -- see this discussion. The long expression that is fed into acos() is evaluating to slightly more than 1.0, which is out of bounds.
Here's the problem illustrated using Python to do the calculations:
>>> from math import sin, cos, acos, radians
>>> lat = radians(45.266708)
>>> long_expression = sin(lat) * sin(lat) + cos(lat) * cos(lat) * cos(0.0)
>>> repr(long_expression)
'1.0000000000000002'
>>> acos(long_expression)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error
>>>
It seems that MySQL is substituting NULL instead of raising an exception. I know little about MySQL, but you should be able to overcome that by doing something like ifnull(acos(long_expression), 0.0) or coalesce(acos(long_expression), 0.0).
Alternatively you could use the haversine formula, which shifts the rounding problem from your door-step to the opposite side of the earth.
Update: I've tested using that formula in Python to calculate the should-be-zero distance between a point and the same point, for each of the 37582 unique (lat, lon) 2-tuples in a file of US zip codes.
Of these:
31591 (84.1%) produced a zero distance
4244 (11.3%) produced a distance of 9.5 cm.
831 (2.2%) produced a distance of 13.4 cm.
916 (2.4%) produced a "cos" value of 1.0000000000000002 which would cause an exception in acos() if not detected and avoided.
It appears that explicit testing for lat1 = lat2 and lon1 = lon2 and avoiding the formula in that case (just use zero) might be a good idea -- it would give a consistent answer and avoid puzzlement.
Related
A library I’m trying to use takes a given latitude and longitude and work out with entries in a table have a lat/lng within a certain distance from this. The generated SQL query works with MySQL, but not with PostgreSQL.
Here’s the entry in my server.log file detailing the error psql is giving and the full query:
ERROR: column "distance" does not exist at character 507
STATEMENT: select *, ( '3959' * acos( cos( radians('53.49') ) * cos( radians( places.lat ) ) * cos( radians( places.lng ) - radians('-2.38') ) + sin( radians('53.49') ) * sin( radians( places.lat ) ) ) ) AS distance from (
Select *
From places
Where places.lat Between 53.475527714192 And 53.504472285808
And places.lng Between -2.4043246788967 And -2.3556753211033
) As places where "places"."deleted_at" is null having "distance" <= $1 order by "distance" asc
Knowing what the SQL should be I can then edit the PHP code that generates it and send a PR back to the library.
The query uses the syntax specific for MySql. In Postgres (and all other known to me RDBMS) you should use a derived table:
select *
from (
select *,
(3959 * acos( cos( radians(53.49) ) * cos( radians( places.lat ) )
* cos( radians( places.lng ) - radians(-2.38) )
+ sin( radians(53.49) ) * sin( radians( places.lat ) ) ) ) AS distance
from (
select *
from places
where places.lat between 53.475527714192 and 53.504472285808
and places.lng between -2.4043246788967 and -2.3556753211033
) as places
where places.deleted_at is null
) sub
where distance <= $1
order by distance asc
I have also removed quotes from numeric constants.
What your query does is a replace the ST_Dwithin function of postgresql with a long query.
ST_DWithin — Returns true if the geometries are within the specified
distance of one another. For geometry units are in those of spatial
reference and For geography units are in meters and measurement is
defaulted to use_spheroid=true (measure around spheroid), for faster
check, use_spheroid=false to measure along sphere.
it's very simple just do
ST_DWithin(geometry1, geometry2)
where geometry1 and geometry2 can be point fields (but they can be other geometric data types two)
So your query might become
SELECT * FROM places WHERE
ST_DWithin(ST_GeomFromText('POINT(-71.060316 48.432044)', 4326), places.geom) ORDER BY ST_Distance (ST_GeomFromText('POINT(-71.060316 48.432044)', 4326), places.geom)
The only change that you will need to do is to put your lat, lng together into a single POINT Field. (your probably should do that even in mysql). You can continue to have your lat,lng in separate columns but you will be sacrificing the gains you can get by using a proper geo type.
Using ST_Dwithin will be faster than a manual distance query because it's designed to make use of geometry columns on postgresql which can be indexed.
What if you want to go back to mysql someday? Well mysql 5.7 added the St_Dwithin feature.
I have a website in Classic ASP with an Access database. I'm using the Google maps API. Now I want to find Companies (from the database) within a radius of 20 km from a given location.
I have found a formular (haversine), but it seems like I can't use ACOS(). I get this error:
Microsoft JET Database Engine error '80040e14'
Undefined function 'acos' in expression.
And here is my sql:
lat = 56.113517
lng = 10.145928
sql = "SELECT companyId, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance FROM company HAVING distance < 25 ORDER BY distance"
Set rs = Connect.Execute(sql)
I have search and search but I cant find a solution.
Can anybody help?
It is true that Access SQL does not have a built-in ACOS() function. If you were running your query in Access itself then you could create your own ACOS() function in VBA to use in an SQL query, but that option is not available to SQL queries against an Access database from other applications (like your ASP app).
So, you will need to do the precise distance calculations in ASP after you retrieve the records that might qualify. That is, you could calculate the latitude and longitude boundaries of a 40km by 40km "square" centered on the target location (to exclude records that are guaranteed to be too far away), then calculate the actual distance for each of the returned records using your ASP function (example code here).
I know there are a lot of questions like mine asked. I looked at them, but I can't seem to resolve the problem. I'm just bad in math :s.
In c# i'm using a SqlQuery to get cities in a range. This works most of the time, except for some cities.
For example, if i ask the nearby cities of the city with postalcode 2060. I get this error. When i ask it for the city with code 2000, it returns 2000 and 2060.
2060: Lat = 51.2293515000 Long = 4.4279883000
2000: Lat = 51.2198771000 Long = 4.4011356000
this is the query:
return base.Database.Database.SqlQuery<City>(
"SELECT * FROM [dbo].[City] WHERE #p0 >= (((acos(sin((#p1*pi()/180)) *
sin(([Latitude]*pi()/180))+cos((#p1*pi()/180)) * cos(([Latitude]*pi()/180)) *
cos(((#p2- [Longitude])*pi()/180))))*180/pi())*60*1.1515*1.609344)"
, radius, latitude, longitude);
Can somebody explain how this can be changed so it works for all the "cities" and the reason if this error for a noob in math?
Thank you
The problem is that the statement inside acos() gets a rounding error somtimes when you compare cities to themselves, and the statement becomes slightly over 1, which causes the error.
You need to cap the value to max 1 somehow. Here is a rather ugly solution:
return base.Database.Database.SqlQuery<City>(
"SELECT * FROM [dbo].[City] WHERE #p0 >= (((acos((select min(v) from (values (1), (sin((#p1*pi()/180)) *
sin(([Latitude]*pi()/180))+cos((#p1*pi()/180)) * cos(([Latitude]*pi()/180)) *
cos(((#p2- [Longitude])*pi()/180)))) as x(v))))*180/pi())*60*1.1515*1.609344)"
, radius, latitude, longitude);
I'm not sure, but it could be that it sometimes gets slightly smaller than -1 as well, which would case the same error. Then you'd have to cap the value to between -1 and 1.
I am using Grails 2.1.1 and have to find all nearest locations for a given point. My code is looking like this (works in my app):
def findNearLocations(double latitude, double longitude) {
def locationsFromQuery = Location.executeQuery(
"SELECT id,(6371 * 2 * ASIN(SQRT(POWER(SIN((:ulatitude - abs(latitude)) * pi()/180 / 2),2) +" +
"COS(:ulatitude * pi()/180 ) * COS(abs(latitude) * pi()/180) *" +
"POWER(SIN((:ulongitude - longitude) * pi()/180 / 2), 2))))*1000 as distance " +
"FROM Location WHERE is_public = TRUE ORDER BY distance", // HAVING distance < 1000
[ulatitude: latitude.toString().toDouble(), ulongitude: longitude.toString().toDouble()])
// do stuff with locationsFromQuery
}
The formula for the computation of the nearest locations I used for is this stack overflow post but I cannot use the HAVING clause like in the comment. If I use it, I get an hql.hibernate exception, saying, that there is an unexpected identifier HAVING. But why this is working in nearly all examples in google but not working for me?
I am using the h2 database in the memory.
This is dragging at my nerves!
Scenario:
I am using the geocoder plugin: http://github.com/alexreisner/geocoder/
I have two models: Address and Item
the latitude and the longitude are in Address and an Item belongs_to :address
Now, when I try to do a near search:
Item.near(Person.first.address.coordinates, 20)
on an Item it fails with the following SQL:
Unknown column 'latitude' in 'field list':
SELECT *, 3956 * 2 * ASIN(SQRT(POWER(SIN((51.3883 - latitude) * PI() / 180 / 2), 2) + COS(51.3883 * PI()/180) * COS(latitude * PI() / 180) * POWER(SIN((6.65326 - longitude) * PI() / 180 / 2), 2) )) AS distance FROM `items` WHERE ((latitude BETWEEN 51.0984449275362 AND 51.6781550724638 AND longitude BETWEEN 6.188777824785 AND 7.117742175215) AND (`items`.`accepted` IS NULL AND `items`.`taken_by` IS NULL)) ORDER BY distance ASC
Don't look at the weird Math stuff in the query,
the problem lies within the FROM and the WHERE clause because
the latitude and the longitude attributes are part of the address table,
so a simple addition to the Query would solve the problem:
... FROM items, addresses WHERE ...
... null)) AND items.address_id = addresses.id ORDER BY ...
I just want to add a table to the FROM field and a condition :(
I tried using several named_scopes using :include and :joins, but none worked!
Does anybody know a solution to this? How could I insert this into a scoped ActiveRecord object?
So the Item has address(es) and that's where the longitude and latitude actually exist, right? That's where I think the query is going wrong for you. You could try adding a :joins=>:address ... but I like the following better:
Since all the math occurs in the database, I think you could do:
Address.near(Person.first.address.coordinates, 20)
and then get the items that the addresses belong to, maybe even an :include=>:item or something.