Перемещение точки по пути в SQL Server 2008 - PullRequest
4 голосов
/ 19 января 2010

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

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

Например, я хочу, чтобы пункт назначения находился в 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 ]

13 голосов
/ 23 января 2010

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

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

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

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

    Иначе:

    ... точка назначения находится дальше на пути от следующей точки в итерации.Вычтите расстояние между точкой 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, если вы предпочитаете.

4 голосов
/ 28 марта 2010

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

1 голос
/ 18 октября 2014

Я использовал ответ Даниэля сверху, но мне пришлось исправить подпись "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
...