Reputation: 17915
Not sure how to solve this problem. I have 2 geography columns
CREATE TABLE #Trip
...
LegRoute geography NULL,
GPSPoint geography NULL,
GPSPointToLegRouteDistance Float NULL,
...
LegRoute
contains polylineGPSPoint
contains pointI can get distance (in miles) from point to line. This is GPS position in relation to path.
UPDATE T
SET GPSPointToLegRouteDistance = LegRoute.STDistance(GPSPoint) / 1609.344
FROM #Trip T
What I need to find is the POINT
to which this distance calculated. Then I need some way to calculate distance from that point to END of polyline.
Real world description:
Vehicle might be off-route (always will). But I need to find closest point on route and how much of this trip distance remaining.
Upvotes: 3
Views: 271
Reputation: 17915
Ok, I did solve this. Feel free to comment and suggest better/faster way. After some research it seems that there is no way to do it with built-in SQL Server functionality. So I resorted to using CLR Integration.
There is a bunch of "assumptions" and solution is not totally accurate from math perspective. But my target was speed. So, I did what I did. Also, in our DB - we store GPS and routes as Lat/Lon and strings (those strings similar to what multiple webservices return, just series of points)
Just by seeing what data we have - we never have individual "legs" more than 1/2-1 mile. And I estimate error being in 0.2 mile at most. Which is for what we do (estimate remaining driving distance/time for over the road trucks) is nothing. Performance is very decent comparing our attempts with geography types. But if you have suggestions on improving C# code speed - please do..
public class Functions
{
/// <summary>
/// Function receives path and current location. We find remaining distance after
/// matching position. Function will return null in case of error.
/// If everything goes well but last point on route is closest to GPS - we return 0
/// </summary>
/// <param name="lat">
/// Latitude of current location
/// </param>
/// <param name="lon">
/// Longitude of current location
/// </param>
/// <param name="path">
/// Path as a series of points just like we have it in RouteCache
/// </param>
/// <param name="outOfRouteMetersThreshhold">
/// Out of route distance we can tolerate. If reached - return NULL
/// </param>
/// <returns>
/// Meters to end of path.
/// </returns>
[SqlFunction]
public static double? RemainingDistance(double lat, double lon, string path, double outOfRouteThreshhold)
{
var gpsPoint = new Point { Lat = lat, Lon = lon };
// Parse path into array of points
// Loop and find point in sequence closest to our input GPS lat/lon
// IMPORTANT!!! There is some simplification of issue here.
// We assume that linestring is pretty granular with small-ish segments
// This way we don't care if distance is to segment. We just check distance to each point.
// This will give better performance but will not be too precise. For what we do - it's OK
var closestPointIndex = 0;
double distance = 10000;
var pointArrayStr = path.Split(',');
if (pointArrayStr.Length < 2) return null;
for (var i = 0; i < pointArrayStr.Length; i++)
{
var latLonStr = pointArrayStr[i].Split(' ');
var currentDistance = DistanceSqrt(
gpsPoint,
new Point { Lat = double.Parse(latLonStr[1]), Lon = double.Parse(latLonStr[0]) });
if (currentDistance >= distance) continue;
distance = currentDistance;
closestPointIndex = i;
}
// Closest point known. Let's see what this distance in meters and handle out of route
var closestPointStr = pointArrayStr[closestPointIndex].Split(' ');
var closestPoint = new Point { Lat = double.Parse(closestPointStr[1]), Lon = double.Parse(closestPointStr[0]) };
var distanceInMeters = DistanceMeters(gpsPoint, closestPoint);
if (distanceInMeters > outOfRouteThreshhold) return null;
// Last point closest, this is "complete" route or something wrong with passed data
if (closestPointIndex == pointArrayStr.Length - 1) return 0;
// Reconstruct path string, but only for remaining points in line
var strBuilder = new StringBuilder();
for (var i = closestPointIndex; i < pointArrayStr.Length; i++) strBuilder.Append(pointArrayStr[i] + ",");
strBuilder.Remove(strBuilder.Length - 1, 1);
// Create geography linestring and calculate lenght. This will be our remaining driving distance
try
{
var geoPath = SqlGeography.STGeomFromText(new SqlChars($"LINESTRING({strBuilder})"), 4326);
var dist = geoPath.STLength().Value;
return dist;
}
catch (Exception)
{
return -1;
}
}
// Compute the distance from A to B
private static double DistanceSqrt(Point pointA, Point pointB)
{
var d1 = pointA.Lat - pointB.Lat;
var d2 = pointA.Lon - pointB.Lon;
return Math.Sqrt(d1 * d1 + d2 * d2);
}
private static double DistanceMeters(Point pointA, Point pointB)
{
var e = Math.PI * pointA.Lat / 180;
var f = Math.PI * pointA.Lon / 180;
var g = Math.PI * pointB.Lat / 180;
var h = Math.PI * pointB.Lon / 180;
var i = Math.Cos(e) * Math.Cos(g) * Math.Cos(f) * Math.Cos(h)
+ Math.Cos(e) * Math.Sin(f) * Math.Cos(g) * Math.Sin(h)
+ Math.Sin(e) * Math.Sin(g);
var j = Math.Acos(i);
var k = 6371 * j; // 6371 earth radius
return k * 1000;
}
private struct Point
{
public double Lat { get; set; }
public double Lon { get; set; }
}
}
Upvotes: 2