Перемещение точки по пути в SQL Server 2008

В моей базе данных хранится поле географии, содержащее путь строки.

Я хочу переместить точку n метров вдоль этой линии и верните пункт назначения.

Например, я хочу, чтобы пункт назначения находился в 500 метрах вдоль линии линии, начиная с ее начала.

Вот пример - что такое YourFunctionHere? Или есть другой способ?

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

3 ответа

Решение

Это немного сложно, но это, безусловно, возможно.

Давайте начнем с расчета подшипника от одной точки к другой. Учитывая начальную точку, направление и расстояние, следующая функция вернет точку назначения:

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

Я понимаю, что вам требуется функция, которая принимает линейную строку в качестве входных данных, а не только начальную и конечную точки. Точка должна двигаться по пути соединенных отрезков и должна продолжать двигаться вокруг "углов" пути. Поначалу это может показаться сложным, но я думаю, что это можно решить следующим образом:

  1. Итерация по каждой точке вашей строки STPointN() от х =1 до х = STNumPoints(),
  2. Найти расстояние с STDistance() между текущей точкой итерации и следующей точкой: @linestring.STPointN(x).STDistance(@linestring.STPointN(x+1))
  3. Если указанное выше расстояние> ваше входное расстояние 'n':

    ... тогда точка назначения находится между этой точкой и следующей. Просто применить func_MoveTowardsPoint проходная точка x в качестве начальной точки, точка x+1 в качестве конечной точки и расстояние n. Вернуть результат и прервать итерацию.

    Else:

    ... точка назначения находится дальше по пути от следующей точки итерации. Вычтите расстояние между точкой x и точкой x+1 из вашего расстояния 'n'. Продолжите итерацию с измененным расстоянием.

Возможно, вы заметили, что мы можем легко реализовать вышеизложенное, а не итеративно.

Давай сделаем это:

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

С этим на месте, пришло время сделать несколько тестов. Давайте используем оригинальную линейную линию, которая была предоставлена ​​в вопросе, и мы запросим пункты назначения на 350 м, на 3500 м и на 7000 м:

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

Наш тест возвращает следующие результаты:

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

Обратите внимание, что последнее запрошенное нами расстояние (7000 м) превысило длину линейной линии, поэтому нам вернули последнюю точку. В этом случае вы можете легко изменить функцию, чтобы она возвращала NULL, если вы предпочитаете.

Существует также функция LocateAlongGeog в библиотеке SQL Spatial Tools на CodePlex http://sqlspatialtools.codeplex.com/wikipage?title=Current%20Contents&referringTitle=Home

Я использовал ответ Даниэля сверху, но мне пришлось исправить подпись "func_MoveAlongPath", чтобы

CREATE FUNCTION [dbo].[func_MoveAlongPath](@path geography, 
                                       @distance **float**, 
                                       @index int = 1)

Int возвращает неправильные результаты, потому что он округляет значения в рекурсивных вызовах. Затем я преобразовал его в итеративную версию, поскольку рекурсивная не могла обрабатывать большие расстояния в данных образца, которые у меня были:

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

    WHILE (@index < @num_points - 1) AND (@comul_distance < @distance)
    BEGIN
        SET @dist_to_next = @path.STPointN(@index).STDistance(@path.STPointN(@index + 1));
        SET @comul_distance += @dist_to_next;
        SET @index += 1;
    END

    SET @result = [dbo].[func_MoveTowardsPoint](@path.STPointN(@index - 1),
                                                        @path.STPointN(@index),
                                                        @distance - (@comul_distance - @dist_to_next));
    RETURN @result;
END
Другие вопросы по тегам