SQL Server: Calculate the Radius of a Lat/Long? - sql-server

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)

Related

Apply STDistance to all rows of table?

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.

how to check Circle latitude longitude

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);

Defining parameters in table valued function - SQL Server

I am trying to return matching zip codes as a table, so I can use it with 'Where zipCode IN(...) statement.
create function dbo.zipSearch(#zip varchar(12), #mile int)
returns table
as
begin
declare #ns float = #mile * 0.00569;
declare #ew float = #mile * 0.01629;
declare #ltt float, #lng float;
Select #ltt = latitude, #lng = longitude From ZipCode Where ZIP = #zip;
Select ZIP From zipcode Where latitude >= #ltt - #ns and latitude <= #ltt + #ns and longitude >= #lng - #ew and longitude <= #lng + #ew;
return
end
What would be the alternative if this is not possible?
Here is how you could turn this into an inline table valued function. The performance benefits might surprise you.
create function dbo.zipSearch(#zip varchar(12), #mile int)returns table as return
Select ZIP
From zipcode
cross apply
(
select latitude
, longitude
from ZipCode
where Zip = #zip
) LatLong
Where latitude >= LatLong.latitude - (#mile * 0.00569)
and latitude <= LatLong.latitude + (#mile * 0.00569)
and longitude >= LatLong.longitude - (#mile * 0.01629)
and longitude <= LatLong.longitude + (#mile * 0.01629);
Here are just a few articles about the differences.
Multi-statement Table Valued Function vs Inline Table Valued Function
http://www.sqlservercentral.com/blogs/discussionofsqlserver/2012/02/15/comparing-inline-and-multistatement-table-valued-functions/
http://sqlmag.com/t-sql/inline-vs-multistatement-table-valued-udfs
Posting the final version of the function in case someone else hits the same wall as I do. Thanks to #Tab Alleman for the tip. It was a syntax issue.
create function dbo.zipSearch(#zip varchar(12), #mile int)
returns #tmp TABLE (zipCode varchar(12))
as
begin
declare #ns float = #mile * 0.00569;
declare #ew float = #mile * 0.01629;
declare #ltt float, #lng float;
Select #ltt = latitude, #lng = longitude From ZipCode Where ZIP = #zip;
INSERT #tmp
Select ZIP From zipcode Where latitude >= #ltt - #ns and latitude <= #ltt + #ns and longitude >= #lng - #ew and longitude <= #lng + #ew;
return
end

Issue calculating distances in google maps only in Panama

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)

Making this spatial query more efficient

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))

Resources