November 28, 2008 at 11:24 am
Good catch. But that leaves me wondering how it converts the results from Great Circle Radians to Miles?
It's not converting from great circle radians to miles - it doesn't need to because the query is just using the result of the trigonometric expression to order the results by the great circle angle between the points. All it has to do is find the minimum angle. In fact you could simplify the trigonometric expression still further (and improve the performance) by omiting the ACOS function, but then you would need to find the maximum rather than the minimum value of the amended trigonometric expression.
CREATE INDEX dbo.Table1_LngLat on dbo.Table1 (Longitude,Latitude)
go
DECLARE @LATITUDE AS FLOAT
DECLARE @LONGITUDE AS FLOAT
SET @LATITUDE = 30.0045
SET @LONGITUDE = -93.8824
SELECT TOP 1 X, Y, Latitude, Longitude
FROM (Select X,Y,Latitude,Longitude
From Table1
Where Longitude between @Longitude-.1 and @Longitude+.1
And Latitude between @Latitude-.1 and @Latitude+.1) A
ORDER BY (SIN(@Latitude/57.295779513082323)
* SIN(Latitude/57.295779513082323)
+ COS(@Latitude/57.295779513082323)
* COS(Latitude/57.295779513082323)
* COS(Longitude/57.295779513082323 - @Longitude/57.295779513082323)) DESC
This trigonometric expression can be derived quite easily from the spherical law of cosines (http://en.wikipedia.org/wiki/Spherical_trigonometry) with the 2 points of interest as two vertices of a spherical triangle and the north pole as the 3rd vertex.
One note of warning: the SARGable box does improve performance, but it causes the query to not find the nearest point if there are no points within this range of +/-0.1 degree latitude and longitude. Also, if the closest points are near to the edge of this range the query might not correctly return the nearest point (in terms of great circle distance).
November 28, 2008 at 11:36 am
andrewd.smith (11/28/2008)
It's not converting from great circle radians to miles - it doesn't need to because the query is just using the result of the trigonometric expression to order the results by the great circle angle between the points. All it has to do is find the minimum angle. In fact you could simplify the trigonometric expression still further (and improve the performance) by omiting the ACOS function, but then you would need to find the maximum rather than the minimum value of the amended trigonometric expression.
Yep, I know all that second part.
And I just noticed that the test proc that I am using is slightly different from what is in my code above:
alter Function [dbo].[CalculateDistance](
@Latitude1 Float,
@Longitude1 Float,
@Latitude2 Float,
@Longitude2 Float
) Returns Float
/*
Faster way to calc distance in miles using Latitude & Longitude
NOTE: 57.295779513082323 = SELECT 180.0 / PI()
*/ As
Begin
/* tests:
select dbo.calculateDistance(31.0, -93.0, 31.1, -93.0)--should be 6.9169 miles
select dbo.calculateDistance(31.0, -93.0, 31.0, -93.1)--should be 5.9290 miles
select dbo.calculateDistance(20.0, -93.0, 20.0, -93.1)--should be 6.4998 miles
select dbo.calculateDistance(40.0, -93.0, 40.0, -93.1)--should be 5.2987 miles
*/
Return 3963.0*acos(
sin(@Latitude1/57.295779513082323)
* sin(@Latitude2/57.295779513082323)
+ cos(@Latitude1/57.295779513082323)
* cos(@Latitude2/57.295779513082323)
* cos(@Longitude2/57.295779513082323 - @Longitude1/57.295779513082323))
End
You notice that " Return 3963.0*acos( " code there? The "3963.0*" is not in my solution query above. And, yes, 3963.0 is the radius of the earth in miles.
:blush:
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
November 28, 2008 at 11:43 am
andrewd.smith (11/28/2008)
One note of warning: the SARGable box does improve performance, but it causes the query to not find the nearest point if there are no points within this range of +/-0.1 degree latitude and longitude.
I believe that I did cover that in one of my previous posts.
Also, if the closest points are near to the edge of this range the query might not correctly return the nearest point (in terms of great circle distance).
Now that is an excellent point that I did miss. The outer query should probably filter out any answers whose distance is greater than 5 miles (which is small enough for the locations in Texas that the OP seems to be using).
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
November 29, 2008 at 7:33 am
Heh... Great Circle calcs to find closest... doesn't seem to be needed especially in the Temperate zone of latitudes and longitudes limited to only 1/8th the circumference of the Earth. There's no real need to measure distance to find closest. Should be simple math as a "matter of degrees". Yep, the method will fail as you approach the poles... anyone know of a UPS or FedEx station above the artic circle?
--Jeff Moden
Change is inevitable... Change for the better is not.
November 29, 2008 at 9:26 am
Jeff Moden (11/29/2008)
Heh... Great Circle calcs to find closest... doesn't seem to be needed especially in the Temperate zone of latitudes and longitudes limited to only 1/8th the circumference of the Earth. There's no real need to measure distance to find closest.
You still have to calculate some kind of metric for comparison, Jeff. The simplest metric of any accuracy is Cartesian, using the Pythagorean solution. Even there, you still have to include a scaling factor for the Latitude to Longitude ratio, which differs significantly based on Latitude.
Once you include all of these things, it is not much less CPU than using the correct calculation and has the distinct advantage of being "always correct" instead of "probably correct". And the kicker is that Distance type queries are almost always dominated by IO concerns anyway (because our indexes are only one-dimensional in nature, not two-dimensional).
I just can't see trying to save microseconds on a query that takes milliseconds at the expense of not being able to say that it is 100% correct.
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
November 29, 2008 at 11:53 am
Heh... milliseconds per calculation mean a lot if you have millions of shipments to make. And you say what's wrong with being 100% correct? Nothing... but you're 100% correct for measuring distance, not just finding the closest which requires no measurement of distance... just the delta in degrees.
What is true about triangles? The hypotenuse has a ratio to the sides no matter how long the sides are. You do not need to calculate the distance (hypotenuse) to determine the closest set of coordinates... just simple math...
ABS(P1x-P2x)+ABS(P1y-P2y) = ClosestFactor
You said it yourself when you talked about finding the "Box" algorithm. If you don't need to know the absolute distance and you pretty much in the Temperate or Tropical zones, you don't need to know what the hypotenuse is never mind the Great Circle hypotenuse to find the closest.
IF you really want to know the distance after that, then apply 1 calculation to the single item you found.
--Jeff Moden
Change is inevitable... Change for the better is not.
November 29, 2008 at 12:27 pm
Jeff Moden (11/29/2008)
What is true about triangles? The hypotenuse has a ratio to the sides no matter how long the sides are. You do not need to calculate the distance (hypotenuse) to determine the closest set of coordinates... just simple math...ABS(P1x-P2x)+ABS(P1y-P2y) = ClosestFactor
No, the hypotenuse does not have a fixed ratio to the sum of the other sides. Rather, the square of the hypotenuse has a fixed ratio(1) to the sum of the squares of the other sides. Take the "squares" out and it is not even close to being true.
For instance, your ABS approximation would incorrectly identify someone who is 6 miles north of you as being closer than someone who was 4 miles north and 3 miles east of you (only 5 miles away (on a flat plane)).
You said it yourself when you talked about finding the "Box" algorithm.
The Box algorithm does not try to determine the closest point. It is just a pre-filter that tries to exclude (in a way that can use an index) all of the locations that are obviously too far away to be the closest. It's a fuzzy filter so it's OK if it is only a gross approximation.
That said, you are right, that we do not need to know the actual distance, which means that we could take off the outer "ACOS(..)" and leave off the "3956*" that I forgot. You cannot go any farther than that though or you are in danger of returning incorrect results.
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
November 29, 2008 at 2:15 pm
rbarryyoung (11/29/2008)
Jeff Moden (11/29/2008)
What is true about triangles? The hypotenuse has a ratio to the sides no matter how long the sides are. You do not need to calculate the distance (hypotenuse) to determine the closest set of coordinates... just simple math...ABS(P1x-P2x)+ABS(P1y-P2y) = ClosestFactor
No, the hypotenuse does not have a fixed ratio to the sum of the other sides. Rather, the square of the hypotenuse has a fixed ratio(1) to the sum of the squares of the other sides. Take the "squares" out and it is not even close to being true.
For instance, your ABS approximation would incorrectly identify someone who is 6 miles north of you as being closer than someone who was 4 miles north and 3 miles east of you (only 5 miles away (on a flat plane)).
You said it yourself when you talked about finding the "Box" algorithm.
The Box algorithm does not try to determine the closest point. It is just a pre-filter that tries to exclude (in a way that can use an index) all of the locations that are obviously too far away to be the closest. It's a fuzzy filter so it's OK if it is only a gross approximation.
That said, you are right, that we do not need to know the actual distance, which means that we could take off the outer "ACOS(..)" and leave off the "3956*" that I forgot. You cannot go any farther than that though or you are in danger of returning incorrect results.
Yeah... you're right... forgot the word "squares" in "sum of the squares". That's what happens when I go too long without wearing the tinfoil hat. 🙂 Thanks for the correction, Barry.
--Jeff Moden
Change is inevitable... Change for the better is not.
November 30, 2008 at 5:08 pm
I've been thinking about this problem over the weekend, and I reckon I've come up with an extension of the method proposed by rbarryyoung that reliably returns the nearest point and retains good performance. Depending on the spatial distribution of points in the database table, it might even improve performance. The procedure still assumes that the distance to the nearest point is significantly smaller than the radius of the earth.
It has the following extra features:
(1) Copes with the meridian at +/-180 longitude, i.e. if the nearest point is on the other side of this meridian, it will not be missed.
(2) Copes with the locations near or at the north and south poles.
(3) Avoids the possibility (I think) that the nearest location will not be returned if the nearest points are near the edge of the pre-filter ("SARG-able") box. This is achieved by features (4) and (6) below.
(4) Ensures that the dimensions of the pre-filter box in the east-west direction is multiplied by a factor 1.0/COS(@Latitude) relative to the north-south direction, which takes account of the fact that the distance represented by 1 degree longitude decreases as you move from the equator to the poles. If this were not done, the shape of the pre-filter box would be elongated in the north-south direction away from the equator, so that a point to the east or west that is outside the box can be nearer than another point to the north or south within the box.
(5) If the pre-filter box is initially too small such that there are no points within the box, the procedure doubles the size of the box until it does contain at least 1 point. Obviously, if the initial size of the box is too small, the number of iterations required will reduce the query performance, so it is important to specify the initial size of the box with care.
(6) If the pre-filter box contains one or more points, then the box is resized based on the point with the minimum sum of the absolute differences in both latitude and longitude:
MIN(ABS(Latitude - @Latitude) + ABS(Longitude - @Longitude))
This procedure may either reduce or increase the size of the box depending of the spatial distribution of points it contains. In order to explain why the box sometimes needs to be increased in size if it already contains one or more points, consider the situation where the box contains a single point close to one of the corners of the box, but a nearer point outside the box that is directly to the north, south, east or west. The expression used here means that the final box dimensions are greater than strictly necessary, but alternative methods of calculating the dimensions are more costly.
(7) Returns the distance to the nearest point taking into account the fact that the radius of the earth varies with latitude. This nicety is not strictly necessary unless you are interested in accuracy, but it doesn't greatly affect the performance of the procedure since it requires a single scalar calculation.
The algorithm is implemented in the following stored procedure.
The initial size of the pre-filter box can be defined in the @dLat parameter. This is half the size of the pre-filter box in the north-south direction in degrees. I've tested this procedure with a test database containing 100,000 records distributed randomly over the whole globe. In this testing the number of spherical trigonometric calculations performed in the final query of the procedure typically varied from 1 to 8, though larger numbers of calculations are performed near to the poles.
CREATE PROCEDURE dbo.NearestLocation
@Latitude float, /* Latitude (-90.0 <= degrees <= 90.0) */
@Longitude float, /* Longitude (-180.0 <= degrees <= 180.0) */
@dLat float = 0.1 /* Latitudinal pre-filter half-range (degrees) */
AS
/* Return immediately if there are no points */
IF NOT EXISTS(SELECT * FROM Table1) RETURN 0
DECLARE @epsilon float
DECLARE @conv float
DECLARE @cosLat float
DECLARE @sinLat float
SET @epsilon = 1.0E-6 /* a small number */
SET @conv = PI() / 180.0 /* multiply to convert from degrees to radians */
SET @cosLat = COS(@Latitude * @conv)
SET @sinLat = SIN(@Latitude * @conv)
/* Calculate radius of earth at latitude @Latitude */
DECLARE @rEquator float /* equatorial radius (km) */
DECLARE @rPolar float /* polar radius (km) */
DECLARE @rGeodetic float /* calculated radius (km) */
SET @rEquator = 6378.1370
SET @rPolar = 6356.7523
SET @rGeodetic = SQRT((POWER(@rEquator * @rEquator * @cosLat, 2.0) + POWER(@rPolar * @rPolar * @sinLat, 2.0))
/ (POWER(@rEquator * @cosLat, 2.0) + POWER(@rPolar * @sinLat, 2.0)))
--SELECT 'Geodetic Radius' = @rGeodetic
DECLARE @count int /* Number of points within box */
DECLARE @dLon float /* Longitudinal pre-filter half-range (degrees) */
DECLARE @dLonWrap float /* Used if pre-filter box overlaps the 180.0 degree meridian */
DECLARE @dLatNext float /* Calculated latitudinal pre-filter half-range (degrees) */
/* Expand pre-filter box in east-west direction to account for the fact that lines of longitude
get closer together towards the poles, and taking account of singularity at poles. */
SET @dLon = CASE
WHEN (@cosLat > @epsilon) THEN @dLat / @cosLat
ELSE 180.0 END
/* Determine if pre-filter box overlaps the 180 degree meridian and set @dLonWrap accordingly */
SET @dLonWrap = CASE
WHEN (@dLon - @Longitude > 180.0) THEN @dLon - @Longitude - 360.0
WHEN (@dLon + @Longitude > 180.0) THEN @dLon + @Longitude - 360.0
ELSE NULL END
/* Iterate until pre-filter box contains at least 1 point */
SET @count = 0
WHILE (@count < 1) BEGIN
SELECT @count = COUNT(*),
@dLatNext = MIN(ABS(Latitude - @Latitude) + ABS(Longitude - @Longitude))
FROM Table1
WHERE (Latitude BETWEEN @Latitude - @dLat AND @Latitude + @dLat)
AND ((Longitude BETWEEN @Longitude - @dLon AND @Longitude + @dLon) OR (Longitude <= @dLonWrap))
--SELECT 'Count' = @count, 'Half Range' = @dLat
/* Resize pre-filter box, doubling size of box if no point found */
SET @dLat = CASE
WHEN (@count >= 1) THEN @dLatNext
ELSE 2.0 * @dLat END
/* Expand pre-filter box in east-west direction to account for the fact that lines of longitude
get closer together towards the poles, and taking account of singularity at poles. */
SET @dLon = CASE
WHEN (@cosLat > @epsilon) THEN @dLat / @cosLat
ELSE 180.0 END
/* Determine if pre-filter box overlaps the 180 degree meridian and set @dLonWrap accordingly */
SET @dLonWrap = CASE
WHEN (@dLon - @Longitude > 180.0) THEN @dLon - @Longitude - 360.0
WHEN (@dLon + @Longitude > 180.0) THEN @dLon + @Longitude - 360.0
ELSE NULL END
END
/* Return the nearest point (including distance in km) using using spherical trigonometry
for those points within the range @Latitude +/- @dLat and @Longitude +/- @dLon */
SELECT TOP (1) X, Y, Latitude, Longitude,
Distance = @rGeodetic * ACOS(@sinLat * SIN(Latitude * @conv) + @cosLat * COS(Latitude * @conv) * COS((Longitude - @Longitude) * @conv))
FROM Table1
WHERE (Latitude BETWEEN @Latitude - @dLat AND @Latitude + @dLat)
AND ((Longitude BETWEEN @Longitude - @dLon AND @Longitude + @dLon) OR (Longitude <= @dLonWrap))
ORDER BY Distance
February 26, 2009 at 11:14 am
I think that you will find that this is not only orders of magnitude faster, but gives the correct results also:
CREATE INDEX dbo.Table1_LngLat on dbo.Table1 (Longitude,Latitude)
go
DECLARE @LATITUDE AS FLOAT
DECLARE @LONGITUDE AS FLOAT
SET @LATITUDE = 30.0045
SET @LONGITUDE = -93.8824
SELECT TOP 1 X, Y, Latitude, Longitude
FROM (Select X,Y,Latitude,Longitude
From Table1
Where Longitude between @Longitude-.1 and @Longitude+.1
And Latitude between @Latitude-.1 and @Latitude+.1) A
ORDER BY acos(sin(@Latitude/57.295779513082323)
* sin(Latitude/57.295779513082323)
+ cos(@Latitude/57.295779513082323)
* cos(Latitude/57.295779513082323)
* cos(Longitude/57.295779513082323 - @Longitude/57.295779513082323))
SWEET.... I've been looking for a query that does this everywhere 🙂
I'd ended up doing the same addition thing with the lat and long to narrow down the list.. It worked well "Enough" but this one is zippy.......
One quick question.... What's the logic behind the bounding box?
Where Longitude between @Longitude-.1 and @Longitude+.1
And Latitude between @Latitude-.1 and @Latitude+.1
Is that just a quick way to thin the results to things only a certain distance away?
I had to remove that part of the query cause it was causing more results to be filtered than I needed
It still runs fast ...
But was just curious
February 26, 2009 at 11:38 am
One quick question.... What's the logic behind the bounding box?
Where Longitude between @Longitude-.1 and @Longitude+.1
And Latitude between @Latitude-.1 and @Latitude+.1
Is that just a quick way to thin the results to things only a certain distance away?
I had to remove that part of the query cause it was causing more results to be filtered than I needed
Yes that's what it is supposed to do. The optimum size of the bounding box depends on your data. If it's too big it will include too many points and the query performance will be affected as there will be many spherical trigonometry calculations. If it's too small, you might not get any results returned at all! In my last post, there is an iterative section (using a WHILE loop) that tries to adjust the size of the bounding box automatically, so that the number of spherical trigonometry calculations is minimised, whilst guaranteeing that the nearest location is inside the bounding box and will be returned by the query. The WHILE loop has the potential to degrade performance, but it can also often improve performance if it is able to adjust the bounding box to a near-optimum size in a small number of iterations.
February 26, 2009 at 12:19 pm
Jeff Moden (11/29/2008)
Heh... milliseconds per calculation mean a lot if you have millions of shipments to make.
Actually, difference is microseconds per calculation, but for the sake of argument, let's call it 1 millisecond per shipment is the average difference in calculation speed. Let's assume 100-million shipments per day. That's 100-thousand seconds per day, which is 27 CPU hours per day. Let's further assume that at 100-million shipments per day, you're operating on at LEAST 32 CPU cores, which comes out to just under 1 hour of actual lost time per day. Let's further assume that calculating the "closest point" incorrectly will take place 1/10th of 1 percent of the time, and will cost 15 minutes of extra work average per time.
Messed up shipments will then cost 1.5-million minutes of lost time per day, at a cost of minimum wage ($6.55) for the drivers having to cover these extra distances (Dogbert runs this company), and we're looking at saving an hour of server time per day at a cost of $156,250 per day in salary for the drivers (not taking into account cost of extra gas, vehicle amortization, vehicle maintenance, extra admin overhead, extra call-center personnel to handle problems with it, etc.). Call it 5 days per week, 50 weeks per year (to take holidays and such into account), and it's $39,062,500 per year.
So, for the cost of having your 32-CPU server spend an extra 50-something minutes per day on your 100-million shipments per day, you can save the company enough money to buy a whole lot more servers and spread the work even more between even more CPUs.
And that is massively exaggerating the cost to the CPUs (which should actually be microseconds per calculation instead of milliseconds) and massively underestimating the cost to the company (min wage drivers, etc.). In real-world terms, spending those few extra microseconds per calculation will save a lot more money than that.
So, sorry Jeff, I'll go with the right results slightly slower over the wrong results slightly faster in this case.
(Disturbingly Complex Math: It's Not Just for Breakfast Any More!)
- Gus "GSquared", RSVP, OODA, MAP, NMVP, FAQ, SAT, SQL, DNA, RNA, UOI, IOU, AM, PM, AD, BC, BCE, USA, UN, CF, ROFL, LOL, ETC
Property of The Thread
"Nobody knows the age of the human race, but everyone agrees it's old enough to know better." - Anon
February 26, 2009 at 12:46 pm
andrewd.smith (2/26/2009)
One quick question.... What's the logic behind the bounding box?
Where Longitude between @Longitude-.1 and @Longitude+.1
And Latitude between @Latitude-.1 and @Latitude+.1
Is that just a quick way to thin the results to things only a certain distance away?
I had to remove that part of the query cause it was causing more results to be filtered than I needed
Yes that's what it is supposed to do. The optimum size of the bounding box depends on your data. If it's too big it will include too many points and the query performance will be affected as there will be many spherical trigonometry calculations. If it's too small, you might not get any results returned at all! In my last post, there is an iterative section (using a WHILE loop) that tries to adjust the size of the bounding box automatically, so that the number of spherical trigonometry calculations is minimised, whilst guaranteeing that the nearest location is inside the bounding box and will be returned by the query. The WHILE loop has the potential to degrade performance, but it can also often improve performance if it is able to adjust the bounding box to a near-optimum size in a small number of iterations.
oh yea :-/ I see that know
apologies on the redundant question and thanks for the response
February 26, 2009 at 1:05 pm
GSquared (2/26/2009)
Jeff Moden (11/29/2008)
Heh... milliseconds per calculation mean a lot if you have millions of shipments to make.Actually, difference is microseconds per calculation, but for the sake of argument, let's call it 1 millisecond per shipment is the average difference in calculation speed. Let's assume 100-million shipments per day. That's 100-thousand seconds per day, which is 27 CPU hours per day. Let's further assume that at 100-million shipments per day, you're operating on at LEAST 32 CPU cores, which comes out to just under 1 hour of actual lost time per day. Let's further assume that calculating the "closest point" incorrectly will take place 1/10th of 1 percent of the time, and will cost 15 minutes of extra work average per time.
Messed up shipments will then cost 1.5-million minutes of lost time per day, at a cost of minimum wage ($6.55) for the drivers having to cover these extra distances (Dogbert runs this company), and we're looking at saving an hour of server time per day at a cost of $156,250 per day in salary for the drivers (not taking into account cost of extra gas, vehicle amortization, vehicle maintenance, extra admin overhead, extra call-center personnel to handle problems with it, etc.). Call it 5 days per week, 50 weeks per year (to take holidays and such into account), and it's $39,062,500 per year.
So, for the cost of having your 32-CPU server spend an extra 50-something minutes per day on your 100-million shipments per day, you can save the company enough money to buy a whole lot more servers and spread the work even more between even more CPUs.
And that is massively exaggerating the cost to the CPUs (which should actually be microseconds per calculation instead of milliseconds) and massively underestimating the cost to the company (min wage drivers, etc.). In real-world terms, spending those few extra microseconds per calculation will save a lot more money than that.
So, sorry Jeff, I'll go with the right results slightly slower over the wrong results slightly faster in this case.
(Disturbingly Complex Math: It's Not Just for Breakfast Any More!)
I guess you didn't see (above) where I said I was wrong, huh? 😉
--Jeff Moden
Change is inevitable... Change for the better is not.
February 26, 2009 at 1:06 pm
Yep, I think that andrewd covered for me just fine here... 🙂
[font="Times New Roman"]-- RBarryYoung[/font], [font="Times New Roman"] (302)375-0451[/font] blog: MovingSQL.com, Twitter: @RBarryYoung[font="Arial Black"]
Proactive Performance Solutions, Inc. [/font][font="Verdana"] "Performance is our middle name."[/font]
Viewing 15 posts - 16 through 30 (of 32 total)
You must be logged in to reply to this topic. Login to reply