I have encountered a problem that is driving me crazy.
A few years ago i developed a browser app that calculate the distance from one given point (latitude and longitude coords) to anothers given points.
Everything has worked fine until a few days ago when a client from Panama started working with us. The same SQL procedure that works for years is giving us wrong measurements.
This is the SQL formula:
(Acos(Sin((Ofd.Latitud * PI()) / 180) * Sin((#Longitud * PI()) / 180) + Cos((Ofd.Latitud * PI()) / 180) * Cos((#Longitud * PI()) / 180) * Cos((Ofd.Logitud * PI() / 180) - (#Latitud * PI()) / 180)) * 6371 * 1000) AS Distance
I tried to calculate the distance using the new method since SQL 2008
DECLARE #Latitude float = 8.9749377
DECLARE #Longitude float = -79.5060562
DECLARE #TLatitude float = 8.9868425
DECLARE #TLongitude float = -79.5012872
DECLARE #Source geography
DECLARE #Target geography
SET #Source = geography::STPointFromText('POINT(' + CAST(#Latitude as varchar(20)) + ' ' + CAST(#Longitude as varchar(20)) + ')',4326)
SET #Target = geography::STPointFromText('POINT(' + CAST(#TLatitude as varchar(20)) + ' ' + CAST(#TLongitude as varchar(20)) + ')',4326)
SELECT #source.STDistance(#Target)
The diference between the two methods is negligible, a few meters. The distance that returns the method is ~500m
So, the problem is that the real distance is almost 1500 meters, I've seen and measured the distance in google maps and 1.500 meters is the real distance. The funny side is that, this problem, only happens in Panama. With the clients in Spain we have no problem calculating the distance.
Have I found the Bermuda's triangle?
You have the Latitude and Longitude reversed. WKT POINT coordinates are ordered X,Y (Longitude, Latitude).
DECLARE #Latitude float = 8.9749377
DECLARE #Longitude float = -79.5060562
DECLARE #TLatitude float = 8.9868425
DECLARE #TLongitude float = -79.5012872
DECLARE #Source geography
DECLARE #Target geography
SET #Source = geography::STPointFromText('POINT(' + CAST(#Longitude as varchar(20)) + ' ' + CAST(#Latitude as varchar(20)) + ')',4326)
SET #Target = geography::STPointFromText('POINT(' + CAST(#TLongitude as varchar(20)) + ' ' + CAST(#TLatitude as varchar(20)) + ')',4326)
SELECT #source.STDistance(#Target)
Related
declare #Latitude DECIMAL(17, 14)='-29.72216606140100',#Longitude DECIMAL(17, 14)='31.06697845459000'
SELECT DISTINCT
*,
pm_Latitude,
pm_Longitude,
CASE
WHEN p.latpoint = pm_Latitude
AND p.longpoint = pm_Longitude
THEN CAST(0 AS DECIMAL(8, 2))
ELSE CAST(p.distance_unit * DEGREES(ACOS(COS(RADIANS(p.latpoint)) * COS(RADIANS(pm_Latitude)) * COS(RADIANS(p.longpoint) - RADIANS(pm_Longitude)) + SIN(RADIANS(p.latpoint)) * SIN(RADIANS(pm_Latitude)))) AS DECIMAL(8, 2))
END AS 'pm_Distance'
FROM Pm_PropertyMapping WITH(NOLOCK) CROSS APPLY
(
SELECT #Latitude AS latpoint,
#Longitude AS longpoint,
100 AS radius,
111.045 AS distance_unit
) AS p
WHERE pm_PropertyName LIKE '%' + 'propertyname' + '%'
AND pm_Latitude BETWEEN p.latpoint - (p.radius / p.distance_unit) AND p.latpoint + (p.radius / p.distance_unit)
AND pm_Longitude BETWEEN p.longpoint - (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint)))) AND p.longpoint + (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
ORDER BY pm_Distance;
What this code does is it searches in a table for the closest property given a specific lat and long.
My question is. Is this the most efficient way of calculating distance in SQL?
I'm trying to return all the rows from [store] with distance of less than 10 miles. Table [Store] has a column of type Geography.
I understand how to find the distance between two specific points, something like this:
declare #origin geography
select #origin = geography::STPointFromText('POINT(' + CAST(-73.935242 AS
VARCHAR(20)) + ' ' + CAST(40.730610 AS VARCHAR(20)) + ')', 4326)
declare #destination geography
select #destination = geography::STPointFromText('POINT(' + CAST(-93.732666 AS VARCHAR(20)) + ' ' + CAST(30.274096 AS VARCHAR(20)) + ')', 4326)
select #origin.STDistance(#destination)/ 1609.344 as 'distance in miles'
I'm having trouble applying this logic to a SELECT statement. Instead of getting the distance between #origin and #destination, I would like to get the distance in miles between #origin and store.Geolocation for all rows.
The STDistance method, used from one instance of Geography and applied to another, returns the distance between the two points. It can be used with variables, e.g. #Origin.STDistance( #Destination ), columns or a combination thereof, e.g. to find all of the stores within 10 miles of a particular #Origin:
select *
from Store
where #Origin.STDistance( Store.Geolocation ) < 1609.344 * 10.0;
Note: As BenThul pointed out, spatial index handling is a bit fickle. An STDistance compared to a constant is SARGable: #Origin.STDistance( Store.Geolocation ) < 1609.344 * 10.0, but this mathematically equivalent expression is not: #Origin.STDistance( Store.Geolocation ) / 1609.344 < 10.0. This "feature" is documented here.
i have following query how to check latitude and longitude are in circle or not but its show wrong result please help
declare #latitudeCar float = 25.681137335685306;
declare #longitudeCar float = 66.697998046875;
declare #latitudePlace float = 25.918526162075153;
declare #longitudePlace float = 66.170654296875;
declare #source geography = 'POINT(' + cast(#latitudeCar as nvarchar) + ' ' + cast(#longitudeCar as nvarchar) + ')'
declare #target geography = 'POINT(' + cast(#latitudePlace as nvarchar) + ' ' + cast(#longitudePlace as nvarchar) + ')'
declare #radius float = 164096.68447201277
declare #check float
declare #isPastCircle bit
set #check = (select #source.STDistance(#target)/1609.344)
--select #source.STDistance(#target)/1000
set #isPastCircle = (select case when #check > #radius then 1 else 0 end);
select #isPastCircle [isPastCircle], #check [carDistance], #radius [acceptableRadius]
Always result is Wrong :(
It looks like you're trying to do your calculations in some fixed radius specified in miles but you're tripping the conversions. Below is a cleaned up version of the code:
declare #latitudeCar float = 25.681137335685306,
#longitudeCar float = 66.697998046875,
#latitudePlace float = 25.918526162075153,
#longitudePlace float = 66.170654296875;
declare #source geography = geography::Point(#latitudeCar, #longitudeCar, 4326),
#target geography = geography::Point(#latitudePlace, #longitudePlace, 4326);
declare #radius_in_miles int = 100;
declare #radius_in_meters float = #radius_in_miles * 1609.344;
select #target.STBuffer(#radius_in_meters).STContains(#source) AS [isWithinCircle],
#target.STDistance(#source) AS [distance_in_meters],
#target.STBuffer(#radius_in_meters).STDisjoint(#source) AS [isPastCircle];
TL;DR - Try, as often as possible, to abstract your conversions away. In this case, I'm specifying my desired radius in the units I'm used to (i.e. miles) and then using a different variable to hold the desired radius that the spatial reference uses (if you're ever in doubt, just query sys.spatial_reference_systems). Also note that I simplified the within/outside of specified radius calculations to use STContains() and STDisjoint() (respectively). I'm assuming that you don't actually care about the distance between the two points, so using those alone should speed things up a bit.
Your query seems correct other than the "<" in the select case. It should be:
set #isPastCircle = (select case when #check > #radius then 1 else 0 end);
Say I have the latitude and longitude of a city and I need to find out all the airport that are within 100 miles of this location. How would I accomplish this? My data resides in SQL Server. 1 table has all the city info with lat and long and the other has the airport info with lat and long.
First ... convert city's data point
DECLARE #point geography;
SELECT geography::STPointFromText('POINT(' + CAST(#lat AS VARCHAR(20)) + ' ' +
CAST(#lon AS VARCHAR(20)) + ')', 4326)
where #lat and #lon are the latitude and longitude of the city in question.
Then you can query the table ...
SELECT [column1],[column2],[etc]
FROM [table]
WHERE #point.STBuffer(160934.4).STIntersects(geography::STPointFromText(
'POINT(' + CAST([lat] AS VARCHAR(20)) + ' ' +
CAST([lon] AS VARCHAR(20)) + ')', 4326) );
where 160934.4 is the number of meters in 100 miles.
This will be slow, though. If you wanted to do even more spatial work, you could add a persisted computed column (because lat and lon points aren't really going to change) and then use a spatial index.
ALTER TABLE [table]
ADD geo_point AS geography::STPointFromText('POINT(' + CAST([lat] AS VARCHAR(20))
+ ' ' + CAST([lon] AS VARCHAR(20)) + ')', 4326) PERSISTED;
CREATE SPATIAL INDEX spix_table_geopt
ON table(geo_point)
WITH ( BOUNDING_BOX = ( 0, 0, 500, 200 ) ); --you'd have to know your data
I used/wrote this several years ago, and it was close enough for what I needed. Part of the formula takes into account the curvature of the earth if I remember correctly, but it has been a while. I used zip codes, but you could easily adapt for cities instead - same logic.
ALTER PROCEDURE [dbo].[sp_StoresByZipArea] (#zip nvarchar(5), #Radius float) AS
DECLARE #LatRange float
DECLARE #LongRange float
DECLARE #LowLatitude float
DECLARE #HighLatitude float
DECLARE #LowLongitude float
DECLARE #HighLongitude float
DECLARE #istartlat float
DECLARE #istartlong float
SELECT #iStartlat=Latitude, #iStartLong=Longitude from zipcodes where zipcode=#ZIP
SELECT #LatRange = #Radius / ((6076 / 5280) * 60)
SELECT #LongRange = #Radius / (((cos((#iStartLat * 3.141592653589 / 180)) * 6076.) / 5280.) * 60)
SELECT #LowLatitude = #istartlat - #LatRange
SELECT #HighLatitude = #istartlat + #LatRange
SELECT #LowLongitude = #istartlong - #LongRange
SELECT #HighLongitude = #istartlong + #LongRange
/** Now you can create a SQL statement which limits the recordset of cities in this manner: **/
SELECT * FROM ZipCodes
WHERE (Latitude <= #HighLatitude) AND (Latitude >= #LowLatitude) AND (Longitude >= #LowLongitude) AND (Longitude <= #HighLongitude)
I have 2 tables:
tZipCodeNoCity with ZipCode and PointGeography
and MBLPosition with Latitude and Longitude
In this query I'm finding closest ZipCode to my positions. It's "poor mans" geocoding :)
How do I write this query so I don't have to do this SELECT TOP 1 inline?
It's pretty slow with even 150 devices (like 20 seconds)
I had to include 150 mile radius into this subselect to get it faster
SELECT LastPositions.DeviceId, P.Description, P.Latitude, P.Longitude, P.Speed, P.DeviceTime,
(
SELECT TOP 1 ZC.ZipCode
FROM dbo.tZipCodeNoCity ZC
WHERE ZC.PointGeography.STDistance(geography::STPointFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326)) < 150 * 1609.344
ORDER BY ZC.PointGeography.STDistance(geography::STPointFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326))
)
FROM dbo.MBLPosition P
INNER JOIN
(
SELECT D.DeviceId, MAX(P.PositionKey) LastPositionKey
FROM dbo.MBLPosition P
INNER JOIN IDATTApplication.dbo.MBLDevice D ON P.DeviceKey = D.DeviceKey
GROUP BY D.DeviceId
) LastPositions ON P.PositionKey = LastPositions.LastPositionKey
In a project I worked on about 12 years ago, I ran a query along these lines to reduce the list of possibilities before doing the actual distance calculation:
WHERE zip.lat < my.lat + 0.5 && zip.lat > my.lat - 0.5
&& zip.long < my.long + 0.5 && zip.long > my.long - 0.5
From that subset, I calculate the actual distance between the two points and sort on it. You'll have to adjust the "0.5" portion as appropriate to get a big enough box to be sure you're going to get a hit.
And I would imagine that there's a better way than STPointFromText to create your point object. Could you use STPointFromWKB? Could you convert to the geography type once?
See this page for an example of creating your point via SET.
DECLARE #p geography;
SET #p = geography::STGeomFromText('POINT(' + CAST(P.Longitude AS VARCHAR(20)) + ' ' + CAST(P.Latitude AS VARCHAR(20)) + ')', 4326);
SELECT TOP 1 ZC.ZipCode
FROM dbo.tZipCodeNoCity ZC
WHERE ZC.PointGeography.STDistance(#p)) < 150 * 1609.344
ORDER BY ZC.PointGeography.STDistance(#p))