SQL Server generating random spatial geography around point? - sql

I have several thousand records in a development environment, each associated with a centroid of a particular zip code. For testing purposes, I need to randomly scatter each SQL Server geography point 0-5 miles around that centroid.
So in the example below I want to update LocationGeo so it is 0-5 miles away from its respective ZipGeo. Do I have to use a random % applied to each Lon/Lat or is there a better option?
LocationID int
LocationGeo geography
ZipCode char(5)
ZipCode char(5)
ZipGeo geography

I found an answer from this post https://gis.stackexchange.com/a/25883/37373
I adapted the response into SQL Server code.
DECLARE #geo as GEOGRAPHY,#newgeo as GEOGRAPHY
SET #geo = (SELECT ZipGeo FROM Location.ZipCodes WHERE ZipCode='90210')
DECLARE #r float,#t float, #w float, #x float, #y float, #u float, #v float;
SET #u=RAND();
SET #v=RAND();
--8046m = ~ 5 miles
SET #r= 8046/(111300*1.0);
SET #w = #r * sqrt(#u);
SET #t = 2 * PI() * #v;
SET #x = #w * cos(#t);
SET #y = #w * sin(#t);
SET #x = #x / cos(#geo.Lat);
SET #newgeo = geography::STPointFromText('POINT('+CAST(#geo.Long+#x AS VARCHAR(MAX))+' '+CAST(#geo.Lat+#y AS VARCHAR(MAX))+')',4326)
--Convert the distance back to miles to validate
SELECT #geo.STDistance(#newgeo)/1609.34

Related

STIntersects in SQL Server Spatial, probably a bug

I show you the conclusion after many headaches and tests.
If we execute the following script in any SSMS session under SQL Server 2012 (also tested in 2017):
DECLARE #g geography;
DECLARE #h geography;
SET #g = geography::STPolyFromText('POLYGON ((-2.2141931466053393 36.848142880725426, -2.1632066297350296 36.864255247830073, 3.0526676842932088 39.266689645726004, 3.168352172454167 39.329935703712941, 3.2286305251469463 39.370418565526464, 3.2322979289615716 39.374091534163213, 3.2372882795963895 39.379457236292687, 3.2583498367577581 39.409984643625563, 3.3506438583660594 39.556107032723332, 3.4300534340816529 39.699235599046659, 3.2674327158297669 42.289964571287427, 3.1599698848294775 42.435144600606137, 0.79329389164208441 42.99441296630598, -7.6685442882152826 43.77780739075277, -8.0476876236197672 43.711457921649725, -9.2115225666636089 43.160194190763086, -9.2984566034556444 43.054102468725411, -9.2715498704469024 42.881894543711283, -7.5169998811285126 37.556527784974641, -7.4389506340710883 37.345028476444256, -7.4300533647696216 37.33933723597093, -7.426814647077407 37.337972629773219, -7.4171018386796526 37.336039770123939, -7.36341431379385 37.33029040340196, -4.5559445740609537 37.01597060730704, -2.2141931466053393 36.848142880725426))', 4326);
SET #h = geography::STPointFromText('POINT (-5.7805724666961673 43.604738856455796)', 4326);
select #g.STIntersects(#h)
We obtain the result 1. Which means that the geometries intersect. Which is not true and you can see clearly if we represent the geometries in GIS tools, such as ArcMap, QGIS or visualize in sites like https://clydedacruz.github.io/openstreetmap-wkt-playground/.
This does not happen only with that point, they can test with others such as the following:
POINT (-5.7808907869201684 43.607612302768608)
POINT (-5.7867532730156022 43.607109291914668)
POINT (-5.7910420343533673 43.607409757130171)
POINT (-5.7962209295114038 43.605527381457819)
POINT (-5.8379991303640395 43.609944466702757)
POINT (-5.8372379698022909 43.613519832305009)
POINT (-5.8339925740272829 43.616976768767834)
POINT (-5.832657139630153 43.620206197447274)
POINT (-5.827899502105284 43.624465756821465)
POINT (-5.8230287455979495 43.6276474699738)
Clarify that it is not a problem of orientation of the ring. Other interior points appear as intersecting.
I have not found an explanation, just a possibility of a bug in SQL server. This makes me very distrustful of the STIntersects () function that I use in countless places in my code.
I would appreciate any response.
After a lot of thinking, my conclusion is that the problem is the projection
used in geography for the operations and graphic representations. If we make the problem with geometry the result is different. You can see here:
DECLARE #g geometry;
DECLARE #h geometry;
SET #g = geometry::STPolyFromText('POLYGON ((-2.2141931466053393 36.848142880725426, -2.1632066297350296 36.864255247830073, 3.0526676842932088 39.266689645726004, 3.168352172454167 39.329935703712941, 3.2286305251469463 39.370418565526464, 3.2322979289615716 39.374091534163213, 3.2372882795963895 39.379457236292687, 3.2583498367577581 39.409984643625563, 3.3506438583660594 39.556107032723332, 3.4300534340816529 39.699235599046659, 3.2674327158297669 42.289964571287427, 3.1599698848294775 42.435144600606137, 0.79329389164208441 42.99441296630598, -7.6685442882152826 43.77780739075277, -8.0476876236197672 43.711457921649725, -9.2115225666636089 43.160194190763086, -9.2984566034556444 43.054102468725411, -9.2715498704469024 42.881894543711283, -7.5169998811285126 37.556527784974641, -7.4389506340710883 37.345028476444256, -7.4300533647696216 37.33933723597093, -7.426814647077407 37.337972629773219, -7.4171018386796526 37.336039770123939, -7.36341431379385 37.33029040340196, -4.5559445740609537 37.01597060730704, -2.2141931466053393 36.848142880725426))', 4326);
SET #h = geometry::STPointFromText('POINT (-5.7805724666961673 43.604738856455796)', 4326);
select #g.STIntersects(#h)
select #g union all select #h
I hope that it is useful for someone.
The problem is not with the STIntersects() method but with the precision that SQL Server uses to store geospatial data, per this blog
SQL Server stores geography and geometry coordinates as binary data, adhering to the IEEE-754 standard for binary floating-point arithmetic. Based on this standard, each coordinate is stored as a double-precision floating-point number that is 64 bits (8 bytes) long.
According to Microsoft Docs
The error tolerance for the geography methods can be as large as 1.0e-7 * extents. The extents refer to the approximate maximal distance between points of the geographyobject.
Using the below code you can see the precision changes.
DECLARE #p1 geography;
DECLARE #p2 geography;
DECLARE #h geography;
SET #p1 = geography::STPointFromText('POINT (-7.6685442882152826 43.77780739075277)', 4326);
SET #p2 = geography::STPointFromText('POINT (0.79329389164208441 42.99441296630598)', 4326);
SET #h = geography::STPointFromText('POINT (-5.7805724666961673 43.604738856455796)', 4326);
select #p1.Lat, #p1.Long
select #p2.Lat, #p2.Long
select #h.Lat, #h.Long
Try running this in SSMS:
SELECT #g UNION ALL SELECT #h
As you can see in the "Spatial results" tab, the point is inside the polygon (regardless of the projection used).

SQL Server Geography

Is there any possible way to improve the below query:
DECLARE #radiusInMeters FLOAT = 400;
DECLARE #dgeog geography = geography::Point(given_latitude, given_longitude, 4326).STBuffer(#radiusInMeters);
select [fdx].latitude, [fdx].longitude
from [dbo].[fdx]
where #dgeog.STIntersects(geography::STGeomFromText('POINT(' + convert(varchar(20), [fdx].longitude) + ' ' + convert(varchar(20), [fdx].latitude) + ')', 4326)
) = 1
kcung and Hasan BINBOGA are correct, you need a spatial index.
Look at your query:
#dgeog.STIntersects(xxxx) = 1
This requires [xxxx] to be a geography data type. In order for [xxxx] to be a geography data type, the STGeomFromText function must be applied to the row. And because this is the only part of your WHERE clause, the function must be applied to all rows.
If the table fdx is particularly large, this means that the CLR function will have to be applied over and over again. This is not (in SQL-Server terms) a fast process.
Try this, if you can:
ALTER dbo.fdx ADD Point AS (GEOGRAPHY::Point(Latitude, Longitude, 4326)) PERSISTED
GO
CREATE SPATIAL INDEX SIndex_FDX ON dbo.fdx (Point)
USING GEOGRAPHY_GRID
WITH (
GRIDS = (LEVEL_1 = HIGH,LEVEL_2 = HIGH,LEVEL_3 = HIGH,LEVEL_4 = HIGH),
CELLS_PER_OBJECT = 1
)
GO
DECLARE #Latitude DECIMAL(15,10) = 0
DECLARE #Longitude DECIMAL(15,10) = 0
DECLARE #Radius FLOAT = 400
DECLARE #g GEOGRAPHY = GEOGRAPHY::Point(#Latitude, #Longitude, 4326).STBuffer(#Radius)
SELECT * FROM dbo.fdx WHERE Point.STIntersects(#g) = 1
A note: You should convert your lat/long pairs into decimals before using them to compute the geography column. There is an implicit conversion from float to decimal to string when you use a float as an input that will trim your coordinates down to 4 decimal places. If you explicitly convert first, that will not be an issue.
Also, if you have any null lat/long values in dbo.fdx, you need to filter them in the WHERE clause as a null value will cause your spatial index not to work properly.
You can create spatial index :
https://msdn.microsoft.com/en-us/library/bb934196.aspx

How can I add a distance in meters to the latitude value in SQL Server 2014

Given
DECLARE #p1 geography
DECLARE #p2 geography
DECLARE #distance int
SET #distance = 10000 -- meters
SET #p1 = geography::Point(51.5001524, -0.1262362, 4326)
How can I set #p2 such that its latitude is #distance meters north or south of #p1?
I have no clue how well this will work in practice, but it can be done just using inbuilt functions.
DECLARE #Distance INT = 10000
DECLARE #latitude DECIMAL(20,10) = 51.5001524
DECLARE #longitude DECIMAL(20,10) = -0.1262362
DECLARE #OriginalPoint GEOGRAPHY = GEOGRAPHY::Point(#latitude, #longitude, 4326)
--This will create a point at the same longitude but at the south pole.
DECLARE #DueSouthPoint GEOGRAPHY = GEOGRAPHY::Point(-90, #longitude, 4326)
DECLARE #SouthLineWithLengthOfDistance GEOGRAPHY =
#OriginalPoint.ShortestLineTo(#DueSouthPoint) --This is a line due south
.STIntersection( --This will return the line segment inside the circle
#OriginalPoint.STBuffer(#distance) --This will draw a circle around the original point with a radius of #distance
)
--Now we have to return the lower point on the line.
--It seems to be fairly inconsistent in which point is first in the line
--I don't want to spend the time to figure it out, so I'm just using a case to determine which point to return.
SELECT
CASE
WHEN #OriginalPoint.STDistance(#SouthLineWithLengthOfDistance.STPointN(1)) = 0
THEN #SouthLineWithLengthOfDistance.STPointN(2)
ELSE #SouthLineWithLengthOfDistance.STPointN(1)
END

Calculating distance between two points (Latitude, Longitude)

I am trying to calculate the distance between two positions on a map.
I have stored in my data: Longitude, Latitude, X POS, Y POS.
I have been previously using the below snippet.
DECLARE #orig_lat DECIMAL
DECLARE #orig_lng DECIMAL
SET #orig_lat=53.381538 set #orig_lng=-1.463526
SELECT *,
3956 * 2 * ASIN(
SQRT( POWER(SIN((#orig_lat - abs(dest.Latitude)) * pi()/180 / 2), 2)
+ COS(#orig_lng * pi()/180 ) * COS(abs(dest.Latitude) * pi()/180)
* POWER(SIN((#orig_lng - dest.Longitude) * pi()/180 / 2), 2) ))
AS distance
--INTO #includeDistances
FROM #orig dest
I don't however trust the data coming out of this, it seems to be giving slightly inaccurate results.
Some sample data in case you need it
Latitude Longitude Distance
53.429108 -2.500953 85.2981833133896
Could anybody help me out with my code, I don't mind if you want to fix what I already have if you have a new way of achieving this that would be great.
Please state what unit of measurement your results are in.
Since you're using SQL Server 2008, you have the geography data type available, which is designed for exactly this kind of data:
DECLARE #source geography = 'POINT(0 51.5)'
DECLARE #target geography = 'POINT(-3 56)'
SELECT #source.STDistance(#target)
Gives
----------------------
538404.100197555
(1 row(s) affected)
Telling us it is about 538 km from (near) London to (near) Edinburgh.
Naturally there will be an amount of learning to do first, but once you know it it's far far easier than implementing your own Haversine calculation; plus you get a LOT of functionality.
If you want to retain your existing data structure, you can still use STDistance, by constructing suitable geography instances using the Point method:
DECLARE #orig_lat DECIMAL(12, 9)
DECLARE #orig_lng DECIMAL(12, 9)
SET #orig_lat=53.381538 set #orig_lng=-1.463526
DECLARE #orig geography = geography::Point(#orig_lat, #orig_lng, 4326);
SELECT *,
#orig.STDistance(geography::Point(dest.Latitude, dest.Longitude, 4326))
AS distance
--INTO #includeDistances
FROM #orig dest
The below function gives distance between two geocoordinates in miles
create function [dbo].[fnCalcDistanceMiles] (#Lat1 decimal(8,4), #Long1 decimal(8,4), #Lat2 decimal(8,4), #Long2 decimal(8,4))
returns decimal (8,4) as
begin
declare #d decimal(28,10)
-- Convert to radians
set #Lat1 = #Lat1 / 57.2958
set #Long1 = #Long1 / 57.2958
set #Lat2 = #Lat2 / 57.2958
set #Long2 = #Long2 / 57.2958
-- Calc distance
set #d = (Sin(#Lat1) * Sin(#Lat2)) + (Cos(#Lat1) * Cos(#Lat2) * Cos(#Long2 - #Long1))
-- Convert to miles
if #d <> 0
begin
set #d = 3958.75 * Atan(Sqrt(1 - power(#d, 2)) / #d);
end
return #d
end
The below function gives distance between two geocoordinates in kilometres
CREATE FUNCTION dbo.fnCalcDistanceKM(#lat1 FLOAT, #lat2 FLOAT, #lon1 FLOAT, #lon2 FLOAT)
RETURNS FLOAT
AS
BEGIN
RETURN ACOS(SIN(PI()*#lat1/180.0)*SIN(PI()*#lat2/180.0)+COS(PI()*#lat1/180.0)*COS(PI()*#lat2/180.0)*COS(PI()*#lon2/180.0-PI()*#lon1/180.0))*6371
END
The below function gives distance between two geocoordinates in kilometres
using Geography data type which was introduced in sql server 2008
DECLARE #g geography;
DECLARE #h geography;
SET #g = geography::STGeomFromText('LINESTRING(-122.360 47.656, -122.343 47.656)', 4326);
SET #h = geography::STGeomFromText('POINT(-122.34900 47.65100)', 4326);
SELECT #g.STDistance(#h);
Usage:
select [dbo].[fnCalcDistanceKM](13.077085,80.262675,13.065701,80.258916)
Reference: Ref1,Ref2
It looks like Microsoft invaded brains of all other respondents and made them write as complicated solutions as possible.
Here is the simplest way without any additional functions/declare statements:
SELECT geography::Point(LATITUDE_1, LONGITUDE_1, 4326).STDistance(geography::Point(LATITUDE_2, LONGITUDE_2, 4326))
Simply substitute your data instead of LATITUDE_1, LONGITUDE_1, LATITUDE_2, LONGITUDE_2 e.g.:
SELECT geography::Point(53.429108, -2.500953, 4326).STDistance(geography::Point(c.Latitude, c.Longitude, 4326))
from coordinates c
Create Function [dbo].[DistanceKM]
(
#Lat1 Float(18),
#Lat2 Float(18),
#Long1 Float(18),
#Long2 Float(18)
)
Returns Float(18)
AS
Begin
Declare #R Float(8);
Declare #dLat Float(18);
Declare #dLon Float(18);
Declare #a Float(18);
Declare #c Float(18);
Declare #d Float(18);
Set #R = 6367.45
--Miles 3956.55
--Kilometers 6367.45
--Feet 20890584
--Meters 6367450
Set #dLat = Radians(#lat2 - #lat1);
Set #dLon = Radians(#long2 - #long1);
Set #a = Sin(#dLat / 2)
* Sin(#dLat / 2)
+ Cos(Radians(#lat1))
* Cos(Radians(#lat2))
* Sin(#dLon / 2)
* Sin(#dLon / 2);
Set #c = 2 * Asin(Min(Sqrt(#a)));
Set #d = #R * #c;
Return #d;
End
GO
Usage:
select dbo.DistanceKM(37.848832506474, 37.848732506474, 27.83935546875, 27.83905546875)
Outputs:
0,02849639
You can change #R parameter with commented floats.
As you're using SQL 2008 or later, I'd recommend checking out the GEOGRAPHY data type. SQL has built in support for geospatial queries.
e.g. you'd have a column in your table of type GEOGRAPHY which would be populated with a geospatial representation of the coordinates (check out the MSDN reference linked above for examples). This datatype then exposes methods allowing you to perform a whole host of geospatial queries (e.g. finding the distance between 2 points)
In addition to the previous answers, here is a way to calculate the distance inside a SELECT:
CREATE FUNCTION Get_Distance
(
#La1 float , #Lo1 float , #La2 float, #Lo2 float
)
RETURNS TABLE
AS
RETURN
-- Distance in Meters
SELECT GEOGRAPHY::Point(#La1, #Lo1, 4326).STDistance(GEOGRAPHY::Point(#La2, #Lo2, 4326))
AS Distance
GO
Usage:
select Distance
from Place P1,
Place P2,
outer apply dbo.Get_Distance(P1.latitude, P1.longitude, P2.latitude, P2.longitude)
Scalar functions also work but they are very inefficient when computing large amount of data.
I hope this might help someone.

SQL Select: toilets within 50KM of a certain position?

I need to find toilets around me, say within 50KM, and I have my position in term of latitude and longitude, toilets in database looks like:
Toilet ID (primary)
Address
Latitude
Longitude
my location: my_lat, my_lon
is it possible to construct a statement that returns all toilets within 50KM of (my_lat, my_lon)? Something like
select * from ToiletTable where
SQRT((Latitude - my_lat)^2 + (Longitude - my_lon)^2) < 50
Thanks!
You are looking for the Haversine formula
Here are two full implementations, one in SQL: Haversine Implementation
EDIT:
Here's a Haversine implementation of a UDF in SQLite. Unfortunately it's against the iPhone, but at least you have the exact implementation you need. Now you just need to determine how to plug it in.
SQL inlined below
CREATE FUNCTION [dbo].[GetDistance]
(
#lat1 Float(8),
#long1 Float(8),
#lat2 Float(8),
#long2 Float(8)
)
RETURNS Float(8)
AS
BEGIN
DECLARE #R Float(8);
DECLARE #dLat Float(8);
DECLARE #dLon Float(8);
DECLARE #a Float(8);
DECLARE #c Float(8);
DECLARE #d Float(8);
SET #R = 6371; --This value is 6371 for kilometers, 3960 for miles.
SET #dLat = RADIANS(#lat2 - #lat1);
SET #dLon = RADIANS(#long2 - #long1);
SET #a = SIN(#dLat / 2) * SIN(#dLat / 2) + COS(RADIANS(#lat1))
* COS(RADIANS(#lat2)) * SIN(#dLon / 2) * SIN(#dLon / 2);
SET #c = 2 * ASIN(MIN(SQRT(#a)));
SET #d = #R * #c;
RETURN #d;
END
GO
Assuming you're not in the polar or pacific regions, i'd use:
where pow(2*(latitude - ?), 2) + pow(longitude - ?, 2) < distance