sql >> Base de Datos >  >> RDS >> Sqlserver

Mover un punto a lo largo de una ruta en SQL Server 2008

Esto es un poco complicado, pero ciertamente es posible.

Comencemos por calcular el rumbo de un punto a otro. Dado un punto de partida, un rumbo y una distancia, la siguiente función devolverá el punto de destino:

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

Entiendo que necesita una función que tome una cadena lineal como entrada, no solo los puntos de inicio y finalización. El punto debe moverse a lo largo de una ruta de segmentos de línea concatenados y debe continuar moviéndose alrededor de las "esquinas" de la ruta. Esto puede parecer complicado al principio, pero creo que se puede abordar de la siguiente manera:

  1. Itera a través de cada punto de tu cadena lineal con STPointN() , de x=1 a x=STNumPoints() .
  2. Encuentre la distancia con STDistance() entre el punto actual en la iteración al siguiente punto:@linestring.STPointN(x).STDistance(@linestring.STPointN(x+1))
  3. Si la distancia anterior> su distancia de entrada 'n':

    ...entonces el punto de destino está entre este punto y el siguiente. Simplemente aplique func_MoveTowardsPoint pasando el punto x como punto inicial, el punto x+1 como punto final y la distancia n. Devuelve el resultado y rompe la iteración.

    Más:

    ...el punto de destino está más lejos en la ruta desde el siguiente punto de la iteración. Reste la distancia entre el punto x y el punto x+1 de su distancia 'n'. Continúe con la iteración con la distancia modificada.

Es posible que haya notado que podemos implementar fácilmente lo anterior de forma recursiva, en lugar de iterativa.

Hagámoslo:

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

Con eso en su lugar, es hora de hacer algunas pruebas. Usemos la cadena de caracteres original que se proporcionó en la pregunta y solicitaremos los puntos de destino a 350 m, a 3500 m y a 7000 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();

Nuestra prueba devuelve los siguientes resultados:

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

Tenga en cuenta que la última distancia que solicitamos (7000 m) excedió la longitud de la línea lineal, por lo que se nos devolvió el último punto. En este caso, puede modificar fácilmente la función para que devuelva NULL, si lo prefiere.