Sqlserver
 sql >> Datenbank >  >> RDS >> Sqlserver

Verschieben eines Punkts entlang eines Pfads in SQL Server 2008

Das ist ein bisschen knifflig, aber durchaus möglich.

Beginnen wir mit der Berechnung der Peilung von einem Punkt zum anderen. Wenn ein Startpunkt, eine Peilung und eine Entfernung gegeben sind, gibt die folgende Funktion den Zielpunkt zurück:

CREATE FUNCTION [dbo].[func_MoveTowardsPoint](@start_point geography,
                                              @end_point   geography,  
                                              @distance    int)  /* Meters */   
RETURNS geography
AS
BEGIN
    DECLARE @ang_dist float = @distance / 6371000.0;  /* Earth's radius */
    DECLARE @bearing  decimal(18,15);
    DECLARE @lat_1    decimal(18,15) = Radians(@start_point.Lat);
    DECLARE @lon_1    decimal(18,15) = Radians(@start_point.Long);
    DECLARE @lat_2    decimal(18,15) = Radians(@end_point.Lat);
    DECLARE @lon_diff decimal(18,15) = Radians(@end_point.Long - @start_point.Long);
    DECLARE @new_lat  decimal(18,15);
    DECLARE @new_lon  decimal(18,15);
    DECLARE @result   geography;

    /* First calculate the bearing */

    SET @bearing = ATN2(sin(@lon_diff) * cos(@lat_2),
                        (cos(@lat_1) * sin(@lat_2)) - 
                        (sin(@lat_1) * cos(@lat_2) * 
                        cos(@lon_diff)));

    /* Then use the bearing and the start point to find the destination */

    SET @new_lat = asin(sin(@lat_1) * cos(@ang_dist) + 
                        cos(@lat_1) * sin(@ang_dist) * cos(@bearing));

    SET @new_lon = @lon_1 + atn2( sin(@bearing) * sin(@ang_dist) * cos(@lat_1), 
                                  cos(@ang_dist) - sin(@lat_1) * sin(@lat_2));

    /* Convert from Radians to Decimal */

    SET @new_lat = Degrees(@new_lat);
    SET @new_lon = Degrees(@new_lon);

    /* Return the geography result */

    SET @result = 
        geography::STPointFromText('POINT(' + CONVERT(varchar(64), @new_lon) + ' ' + 
                                              CONVERT(varchar(64), @new_lat) + ')', 
                                   4326);

    RETURN @result;
END

Ich verstehe, dass Sie eine Funktion benötigen, die eine Linienfolge als Eingabe verwendet, nicht nur Start- und Endpunkte. Der Punkt muss sich entlang eines Pfades aus verketteten Liniensegmenten bewegen und muss sich weiter um die "Ecken" des Pfades bewegen. Dies mag zunächst kompliziert erscheinen, aber ich denke, es kann wie folgt angegangen werden:

  1. Durchlaufen Sie jeden Punkt Ihrer Linienfolge mit STPointN() , von x=1 bis x=STNumPoints() .
  2. Finden Sie die Entfernung mit STDistance() zwischen dem aktuellen Punkt in der Iteration zum nächsten Punkt:@linestring.STPointN(x).STDistance(@linestring.STPointN(x+1))
  3. Wenn die obige Distanz> Ihre eingegebene Distanz 'n':

    ...dann liegt der Zielpunkt zwischen diesem und dem nächsten Punkt. Wenden Sie einfach func_MoveTowardsPoint an Durchgangspunkt x als Startpunkt, Punkt x+1 als Endpunkt und Distanz n. Geben Sie das Ergebnis zurück und brechen Sie die Iteration ab.

    Sonst:

    ...der Zielpunkt liegt weiter im Pfad vom nächsten Punkt in der Iteration. Subtrahiere den Abstand zwischen Punkt x und Punkt x+1 von deinem Abstand 'n'. Fahren Sie mit der geänderten Distanz durch die Iteration fort.

Sie haben vielleicht bemerkt, dass wir das obige einfach rekursiv statt iterativ implementieren können.

Lass es uns tun:

CREATE FUNCTION [dbo].[func_MoveAlongPath](@path geography, 
                                           @distance int, 
                                           @index int = 1)   
RETURNS geography
AS
BEGIN
    DECLARE @result       geography = null;
    DECLARE @num_points   int = @path.STNumPoints();
    DECLARE @dist_to_next float;

    IF @index < @num_points
    BEGIN
        /* There is still at least one point further from the point @index
           in the linestring. Find the distance to the next point. */

        SET @dist_to_next = @path.STPointN(@index).STDistance(@path.STPointN(@index + 1));

        IF @distance <= @dist_to_next 
        BEGIN
            /* @dist_to_next is within this point and the next. Return
              the destination point with func_MoveTowardsPoint(). */

            SET @result = [dbo].[func_MoveTowardsPoint](@path.STPointN(@index),
                                                        @path.STPointN(@index + 1),
                                                        @distance);
        END
        ELSE
        BEGIN
            /* The destination is further from the next point. Subtract
               @dist_to_next from @distance and continue recursively. */

            SET @result = [dbo].[func_MoveAlongPath](@path, 
                                                     @distance - @dist_to_next,
                                                     @index + 1);
        END
    END
    ELSE
    BEGIN
        /* There is no further point. Our distance exceeds the length 
           of the linestring. Return the last point of the linestring.
           You may prefer to return NULL instead. */

        SET @result = @path.STPointN(@index);
    END

    RETURN @result;
END

Damit ist es an der Zeit, einige Tests durchzuführen. Verwenden wir die ursprüngliche Linienfolge, die in der Frage bereitgestellt wurde, und fordern die Zielpunkte auf 350 m, 3500 m und 7000 m an:

DECLARE @g geography;
SET @g = geography::STGeomFromText('LINESTRING(-122.360 47.656, 
                                               -122.343 47.656, 
                                               -122.310 47.690)', 4326);

SELECT [dbo].[func_MoveAlongPath](@g, 350, DEFAULT).ToString();
SELECT [dbo].[func_MoveAlongPath](@g, 3500, DEFAULT).ToString();
SELECT [dbo].[func_MoveAlongPath](@g, 7000, DEFAULT).ToString();

Unser Test gibt die folgenden Ergebnisse zurück:

POINT (-122.3553270591861 47.6560002502638)
POINT (-122.32676470116748 47.672728464582583)
POINT (-122.31 47.69)

Beachten Sie, dass die letzte von uns angeforderte Entfernung (7000 m) die Länge der Linienfolge überschritten hat, sodass uns der letzte Punkt zurückgegeben wurde. In diesem Fall können Sie die Funktion einfach ändern, um NULL zurückzugeben, wenn Sie dies bevorzugen.