Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Déplacement d'un point le long d'un chemin dans SQL Server 2008

C'est un peu délicat, mais c'est certainement possible.

Commençons par calculer le relèvement d'un point à un autre. Étant donné un point de départ, un relèvement et une distance, la fonction suivante renverra le point de destination :

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

Je comprends que vous avez besoin d'une fonction qui prend une chaîne de lignes en entrée, pas seulement des points de début et de fin. Le point doit se déplacer le long d'un chemin de segments de ligne concaténés et doit continuer à se déplacer autour des "coins" du chemin. Cela peut sembler compliqué au premier abord, mais je pense qu'il peut être abordé comme suit :

  1. Parcourez chaque point de votre chaîne de lignes avec STPointN() , de x=1 à x=STNumPoints() .
  2. Trouvez la distance avec STDistance() entre le point actuel de l'itération et le point suivant :@linestring.STPointN(x).STDistance(@linestring.STPointN(x+1))
  3. Si la distance ci-dessus> votre distance d'entrée 'n' :

    ...alors le point de destination se situe entre ce point et le suivant. Appliquez simplement func_MoveTowardsPoint passant le point x comme point de départ, le point x+1 comme point final et la distance n. Renvoie le résultat et interrompt l'itération.

    Sinon :

    ... le point de destination est plus loin dans le chemin du point suivant dans l'itération. Soustrayez la distance entre le point x et le point x+1 de votre distance 'n'. Continuez l'itération avec la distance modifiée.

Vous avez peut-être remarqué que nous pouvons facilement implémenter ce qui précède de manière récursive, au lieu de manière itérative.

Allons-y :

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

Avec cela en place, il est temps de faire quelques tests. Utilisons la chaîne de lignes d'origine fournie dans la question, et nous demanderons les points de destination à 350 m, à 3 500 m et à 7 000 m :

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

Notre test renvoie les résultats suivants :

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

Notez que la dernière distance que nous avons demandée (7000 m) dépassait la longueur de la ligne, nous avons donc renvoyé le dernier point. Dans ce cas, vous pouvez facilement modifier la fonction pour renvoyer NULL, si vous préférez.