Сохранить стартовые точки в UnionAggregate

Вариант использования 1:

DECLARE @Geom TABLE 
( 
   shape geometry, 
   shapeType nvarchar(50) 
); 

INSERT INTO @Geom(shape,shapeType) 
VALUES('LINESTRING(1 2, 3 4)', 'A'), 
('LINESTRING(3.2 4, 7 8)', 'B'); 

SELECT *
FROM @Geom

SELECT geometry::UnionAggregate(shape).ToString(), geometry::UnionAggregate(shape)
FROM @Geom;

WKT для вывода

MULTILINESTRING ((7 8, 3.2 4), (3 4, 1 2))

когда я хотел бы

MULTILINESTRING ((1 2, 3 4), (3.2 4, 7 8))

Где начало строки "A" и "B" должно быть (1 2) и (3.2 4) соответственно.

Такое поведение UnionAggregate, похоже, не заботится о «направлении» геометрии, чтобы утверждать, что объединение A B и объединение B A является одним и тем же результатом. Однако я хочу сохранить начальную/конечную точки, так как я объединяю геометрию улиц, и я хочу, чтобы все LINESTRING шли в их первоначальном направлении.

Эта проблема обсуждается здесь: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/89e95366-3649-4294-a0bc-f3921598157f/union-of-linestrings-and-reversing-direction?forum=sqlspatial

Они как бы подсказывают возможное решение по проверке конечного результата, но мне непонятно, как это сделать. На это намекают в связанной ветке, что

The MultiLineString always represents the graph from the point which farthest from the origin point.

Мне не совсем ясно, что это значит, но я не думаю, что могу просто предположить, что результат UnionAggregate всегда противоположен тому, что я хочу

Если трудно определить направленное намерение, я могу добавить M показателей, где направление должно соответствовать возрастающим значениям M.

Предполагая, что у меня есть метод обращения точек в строке, как мне решить эту проблему?

Я нашел функцию, которая имитирует STUnion для дополнительной поддержки измерения Z и M: http://www.spatialdbadvisor.com/files/SQLServer.html#robo48, однако отмечено, что «их направление может измениться (например, отношение Start/Start Point)», чего я хочу избежать.

Редактировать:

Функциональность, которая мне также нужна, заключается в том, что когда LINESTRING имеет общую конечную точку, результатом является подключение LINESTRING

Вариант использования 2:

DECLARE @Geom TABLE 
( 
   shape geometry, 
   shapeType nvarchar(50) 
); 

INSERT INTO @Geom(shape,shapeType) 
VALUES('LINESTRING(1 2, 3 4)', 'A'), 
('LINESTRING(3 4, 7 8)', 'B'); 

SELECT *
FROM @Geom

SELECT geometry::UnionAggregate(shape).ToString(), geometry::UnionAggregate(shape)
FROM @Geom;

Это приводит к WKT LINESTRING (7 8, 3 4, 1 2)

Когда я хотел бы

LINESTRING (1 2, 3 4, 7 8)


Попытка решения

geometry::CollectionAggregate(shape).Reduce(0), предложенный Клэем, решает вариант использования 1. Я попытался просто использовать STUnion для результата с пустой строкой, и пока он работает, он возвращается к неправильному порядку.

Я подозреваю, что решением будет функция масштабирования, похожая на ST_LineMerge, которая берет результат CollectionAggregate (MULTILINESTRING), а затем объединяет точки вместе, когда это возможно, в одну LINESTRING, а когда это невозможно, возвращает геометрию без изменений.

It is not clear to me exactly what this means "The MultiLineString always represents the graph from the point which farthest from the origin point." Отправная точка (0,0). Итак, результат показывает (7,8) первым, что дальше от (0,0), чем (3.2,4). Результат показывает (3,4) первым, потому что он дальше от (0,0), чем (1,2). Таким образом, результат не меняет исходное направление и не является случайным, он довольно хорошо определен.
Vladimir Baranov 18.03.2019 09:57
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
1
780
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Типы геометрии не записывают/кодируют направленность. Линии, которые вы ему даете, могут считаться «ненаправленными» или «двунаправленными». Это возвращает 1:

select geometry::STGeomFromText('LINESTRING(1 2, 3 4)',0).STEquals(
       geometry::STGeomFromText('LINESTRING(3 4, 1 2)',0))

Итак, то, что вы ищете, недоступно с использованием этих типов. Вы считаете «точки старта» особенными. Я предлагаю вам отдельно записать их как отдельные POINTs.

Тем не менее, это делает весь результирующий код более уродливым — вы должны обрабатывать эти пары данных вместе:

DECLARE @Geom TABLE 
(
   start geometry, 
   shape geometry, 
   shapeType nvarchar(50) 
); 

INSERT INTO @Geom(start,shape,shapeType) 
VALUES('POINT(1 2)','LINESTRING(1 2, 3 4)', 'A'), 
('POINT(3.2 4)','LINESTRING(3.2 4, 7 8)', 'B'); 

SELECT *
FROM @Geom

SELECT
    geometry::UnionAggregate(start).ToString(), geometry::UnionAggregate(shape).ToString(),
    geometry::UnionAggregate(start), geometry::UnionAggregate(shape)
FROM @Geom;

На этом этапе вы мая решаете прекратить прямое использование типа geography — вы можете создать CLR UDT, который ссылается на SqlGeography (поверхность CLR того же типа) и использует его внутри, но также отслеживает его «направленность», все вместе, и начните использовать это вместо этого.

Однако вряд ли вы захотите раскрыть все методы geography в этой обертке — вам придется выбирать свои сражения. И, конечно же, поскольку в ваших результатах появляется не SQL Server geography, вы не сможете воспользоваться вкладкой «Пространственные результаты» в Management Studio.


Единственное место, где, как мне кажется, существует некоторая «направленность» в этих типах, — это правило левой руки для устранения неоднозначности форм geography.

После того, как я изогнулся в сторону, чтобы выполнить манипуляции со строками, я должен был согласиться с тем, что CLR — лучшее место для выполнения такой работы.

Clay 22.03.2019 19:24
Ответ принят как подходящий

Изначально я предложил...

DECLARE @Geom TABLE 
( 
   shape geometry, 
   shapeType nvarchar(50) 
); 

INSERT @Geom(shape,shapeType) VALUES
  ('LINESTRING(1 2, 3 4)', 'A'), 
  ('LINESTRING(3.2 4, 7 8)', 'B'); 

SELECT * FROM @Geom

SELECT 
  geometry::CollectionAggregate(shape).Reduce(0).ToString(), 
  geometry::CollectionAggregate(shape).Reduce(0)
FROM @Geom

Ты получаешь:

... однако мне стало ясно, что ответ, который я дал, недостаточно хорош. Например, довольно сложно удержать Reduce() от упрощения части ваших строк,

Мне по-прежнему нравится CollectionAggregate за объединение исходного массива строк в одну вещь, но потом я подумал, что должен быть способ построить необходимую геометрическую структуру.

Я играл с этим несколько раз, и эта итерация будет оцениваться как LineString или MultiLineString в зависимости от того, есть ли во входных данных непересекающиеся LineString элементы:

create function dbo.SimplifyToLine( @geo geometry ) returns geometry as
begin
  declare 
    @numSubGeos int = @geo.STNumGeometries(),
    @subGeoIdx int = 1,
    @sql nvarchar( max ) = N'',
    @subGeo geometry,
    @oldEndX float = -1.0e26,
    @oldEndY float = -1.0e26,
    @startX float,
    @startY float,
    @endX float,
    @endY float,
    @idx int,
    @numPoints int,
    @point geometry,
    @segment int = 1,
    @continue bit,
    @result geometry,
    @started bit = 0

  declare
    @geos table
    ( 
      Idx int primary key, 
      SubGeo geometry, 
      StartX decimal, 
      EndX decimal, 
      StartY decimal, 
      EndY decimal, 
      NumPoints int, 
      ContinueFromPrevious bit 
    ) 

  declare
    @multiLines table
    (
      Idx int primary key,
      Segment nvarchar(max)
    )

  --> collect geometries and extents...
  while ( @subGeoIdx <= @numSubGeos )  
  begin

    select @subGeo = @geo.STGeometryN( @subGeoIdx )

    select 
      @startX = @subGeo.STPointN( 1 ).STX,
      @startY = @subGeo.STPointN( 1 ).STY,
      @endX = @subGeo.STPointN( @subGeo.STNumPoints( ) ).STX,
      @endY = @subGeo.STPointN( @subGeo.STNumPoints( ) ).STY

    insert @geos values
    ( 
      @subGeoIdx,
      @subGeo, 
      @startX, 
      @endX, 
      @startY, 
      @endY, 
      @subGeo.STNumPoints() ,
      case when @subGeoIdx = 1 then 1 when @oldEndX = @startX and @oldEndY = @startY then 1 else 0 end
    )   

    select 
      @oldEndX = @endX, 
      @oldEndY = @endY, 
      @subGeoIdx = @subGeoIdx + 1
  end


  if not exists ( select * from @geos where ContinueFromPrevious = 0 ) --> then all LineStrings are connected 
  begin
    --> build a single LINESTRING( )...
    select @sql = ''
    declare c cursor for select SubGeo, StartX, EndX, StartY, EndY, NumPoints, ContinueFromPrevious from @geos order by Idx  
    open c
    while ( 1 = 1 )
    begin
      fetch next from c into @subGeo, @startX, @endX, @startY, @endY, @numPoints, @continue
      if @@fetch_status != 0 break;
      select @idx = case when @started = 0 then 1 else 2 end, @started = 1  --> accrue all points, de-duplicating line ends...
      while ( @idx <= @numPoints )
      begin
        select @point = @subGeo.STPointN( @idx )
        select @sql += convert( nvarchar, @point.STX ) + N' ' + convert( nvarchar, @point.STY ) + N','
        select @idx = @idx + 1
      end
    end
    close c
    deallocate c
    select @sql = substring( @sql, 1, len( @sql ) -1 )
    select @result =  geometry::STGeomFromText(N'LINESTRING(' + @sql + N')', 0 ) 
  end
  else  --> we have disjoint lines in the inputs...
  begin
    select @sql = N'', @started = 0
    --> build a MULTILINESTRING((),()...) with line segements terminated at disjoint points..
    declare c cursor for select SubGeo, StartX, EndX, StartY, EndY, NumPoints, ContinueFromPrevious from @geos  order by Idx
    open c
      while ( 1=1 )
      begin
        fetch next from c into @subGeo, @startX, @endX, @startY, @endY, @numPoints, @continue
        if @@fetch_status != 0 break;
        if @continue = 1
        begin
          select @idx = case when @started = 0 then 1 else 2 end, @started = 1
          while ( @idx <= @numPoints )
          begin
            select @point = @subGeo.STPointN( @idx )
            select @sql += convert( nvarchar, @point.STX ) + N' ' + convert( nvarchar, @point.STY ) + N','
            select @idx = @idx + 1
          end
        end
        else
        begin
          insert @multiLines values ( @segment, substring( @sql, 1, len( @sql ) -1 ) ) --> collect the segment
          select @idx = 1, @sql = N'', @segment = @segment + 1
          while ( @idx <= @numPoints )
          begin
            select @point = @subGeo.STPointN( @idx )
            select @sql += convert( nvarchar, @point.STX ) + N' ' + convert( nvarchar, @point.STY ) + N','
            select @idx = @idx + 1
          end
        end
      end
    close c
    deallocate c
    insert @multiLines values ( @segment, substring( @sql, 1, len( @sql ) -1 ) )
    select @sql = N''
    select @sql += N'(' + Segment + N'),' from @multiLines order by Idx --> appends all segments
    select @sql = substring( @sql, 1, len( @sql ) -1 )
    select @result = geometry::STGeomFromText( 'MULTILINESTRING('+ @sql + N')', 1 )
  end

... и, наконец, учитывая:

DECLARE @Geom TABLE 
( 
   shape geometry, 
   shapeType nvarchar(50) 
); 

INSERT @Geom(shape,shapeType) VALUES
  ('LINESTRING(1 2, 3 4)', 'A'), 
  ('LINESTRING(3 4, 9 9)', 'B'),  --> disjoint from here to the next LINESTRING
  ('LINESTRING(9 8, 3 4)', 'C'),
  ('LINESTRING(3 4, 1 2)', 'D'); 

select 
  dbo.SimplifyToLine(geometry::CollectionAggregate(shape)).ToString(),
  dbo.SimplifyToLine(geometry::CollectionAggregate(shape))
from 
  @Geom

delete @Geom

INSERT @Geom(shape,shapeType) VALUES
('LINESTRING(1 2, 3 4)', 'A'), 
('LINESTRING(3 4, 9 8)', 'B'),
('LINESTRING(9 8, 3 4)', 'C'),
('LINESTRING(3 4, 1 2)', 'D'); 

select 
  dbo.SimplifyToLine(geometry::CollectionAggregate(shape)).ToString(),
  dbo.SimplifyToLine(geometry::CollectionAggregate(shape))
from
  @Geom

...ты получаешь:

this

Это почти то, что мне нужно, это соответствует моему варианту использования, как указано в исходном вопросе, но, к сожалению, результат не такой, как у UnionAggregate, так что, если две строки соединены, он по-прежнему сохраняет их MULTILINESTRING вместо сокращения в LINESTRING. Будет обновлен вопрос, чтобы отразить это

ParoX 18.03.2019 15:34

Я считаю, что решение будет включать ваше предложение. В настоящее время, если никто не ответит с альтернативами, я награжу вас баллами, если кто-то ответит, я разделю баллы.

ParoX 18.03.2019 18:25

При использовании моего ввода 1 2, 3 4, 7 8 он возвращает только 3 4, 7 8.

ParoX 21.03.2019 06:50

Да, я тоже видел, где я это сделал. Я редактировал его несколько раз, и похоже, что я исправил его, так как вы получили код.

Clay 21.03.2019 12:18

Исправлена ​​ошибка, из-за которой неправильно удалялись дубликаты точек на стороне MultiLineString. Спасибо за наводку, кстати ;-)

Clay 21.03.2019 13:32

Отталкиваясь от идеи Клея о передаче GeometryCollection, я реализовал надежную версию, которая будет принимать любую комбинацию POINT, MULTIPOINT, LINESTRING, MULTILINESTRING и удалять любые соприкасающиеся конечные точки внутри @Tolerance и создавать POINT, LINESTRING, MULTILINESTRING

Вот демонстрация того, как это работает (обратите внимание, как допуск 0 и 0,1 влияет на 2-й и 3-й выход):

DECLARE @GeometryCollection GEOMETRY = GEOMETRY::STGeomFromText('GEOMETRYCOLLECTION (LINESTRING (1 2, 3 4), LINESTRING (3 4, 100 100), LINESTRING (9 8, 3 4), LINESTRING (3 4, 1 2), POINT(1 2), POINT(1 2), POINT(1 2))',0)
SELECT [dbo].[fnSimplifyToLine](@GeometryCollection, 0).ToString();
--Output: MULTILINESTRING ((1 2, 3 4, 100 100), (9 8, 3 4, 1 2))

SET @GeometryCollection  = GEOMETRY::STGeomFromText('GEOMETRYCOLLECTION (LINESTRING (1 2, 3 4.1), LINESTRING (3 4, 9 9, 6 1))',0)
SELECT [dbo].[fnSimplifyToLine](@GeometryCollection, 0).ToString()
--Output: MULTILINESTRING ((1 2, 3 4.1), (3 4, 9 9, 6 1))

SET @GeometryCollection  = GEOMETRY::STGeomFromText('GEOMETRYCOLLECTION (LINESTRING (1 2, 3 4.1), LINESTRING (3 4, 9 9, 6 1))',0)
SELECT [dbo].[fnSimplifyToLine](@GeometryCollection, 0.1).ToString()
--Output: LINESTRING (1 2, 3 4.1, 9 9, 6 1)

SET @GeometryCollection  = GEOMETRY::STGeomFromText('GEOMETRYCOLLECTION (POINT(1 2))',0)
SELECT [dbo].[fnSimplifyToLine](@GeometryCollection, 0).ToString()
--Output: POINT (1 2)

SET @GeometryCollection  = GEOMETRY::STGeomFromText('GEOMETRYCOLLECTION (MULTIPOINT((1 2), (2 3)))',0)
SELECT [dbo].[fnSimplifyToLine](@GeometryCollection, 0).ToString()
--Output:  (1 2, 2 3)

Сначала мне пришлось создать рекурсивную функцию CTE, которая берет геометрию и извлекает все точки.

CREATE FUNCTION [dbo].[fnGetPoints]
(   
    @Geometry GEOMETRY
)
RETURNS TABLE 
AS
RETURN 
(

    WITH GeometryPoints(N, Point) AS ( 
        SELECT 
            CAST(1 AS DECIMAL(9,2)) as N
            ,@Geometry.STPointN(1) as Point
        UNION ALL
        SELECT 
            CAST(N + 1.0 AS DECIMAL(9,2)) as N
            ,@Geometry.STPointN(N + 1) as Point
        FROM GeometryPoints GP
        WHERE N < @Geometry.STNumPoints()  
    )

    SELECT *
    FROM GeometryPoints
)

Затем я создал функцию, которая ПРИМЕНЯЕТ fnGetPoints к каждой геометрии в @GeometryCollection, чтобы получить матрицу точек. Использование оконной функции (LAG), чтобы найти места, где конечные точки находятся внутри @Tolerance, и удалить эти точки. Затем я сделал размазывание данных, чтобы объединить геометрии, где они имеют общие конечные точки.

CREATE FUNCTION [dbo].[fnSimplifyToLine] (@GeometryCollection GEOMETRY, @Tolerance DECIMAL(19,10))
RETURNS GEOMETRY
AS
BEGIN
    DECLARE @PointMatrix TABLE (
        PointId INT,
        LinestringId INT,
        GeometryIndex INT,
        GeometryType varchar(100),
        PointIndex INT,
        Point GEOMETRY,
        Duplicate BIT
    );

    DECLARE @Linestrings TABLE (
        LinestringId INT,
        PointArrayStr varchar(max)
    );

    WITH CollectionGeometries(N, Geom) AS ( 
        SELECT 
            CAST(1 AS DECIMAL(9,2)) as N
            ,@GeometryCollection.STGeometryN(1) as Geom
        UNION ALL
        SELECT 
            CAST(N + 1.0 AS DECIMAL(9,2)) as N
            , @GeometryCollection.STGeometryN(N + 1) as Geom
        FROM CollectionGeometries CG
        WHERE N < @GeometryCollection.STNumGeometries()
    ), PointMatrix AS (
        SELECT 
            ROW_NUMBER() OVER(ORDER BY G.N, P.N) as PointId
            ,G.N as GeometryIndex
            ,G.Geom.STGeometryType() as GeometryType
            ,P.N as PointIndex
            ,P.Point
        FROM CollectionGeometries G
        CROSS APPLY dbo.fnGetPoints(Geom) P
    )

    INSERT INTO @PointMatrix
    SELECT 
        PointId
        ,GeometryIndex as LinestringId
        ,GeometryIndex
        ,GeometryType
        ,PointIndex
        ,Point
        ,CASE 
            WHEN 
                GeometryIndex != LAG(GeometryIndex) OVER(ORDER BY PointId)
                AND ABS(Point.STX - LAG(Point.STX) OVER(ORDER BY PointId)) <= @Tolerance
                AND ABS(Point.STY - LAG(Point.STY) OVER(ORDER BY PointId)) <= @Tolerance
            THEN 1
            ELSE 0
        END as Duplicate
    FROM PointMatrix
    OPTION (MAXRECURSION 10000)



    -- POLYGON, MULTIPOLYGON, GEOMETRYCOLLECTION, CIRCULARSTRING, COMPOUNDCURVE, CURVEPOLYGON not supported
    IF EXISTS ( SELECT * FROM @PointMatrix WHERE GeometryType NOT IN ('POINT', 'MULTIPOINT', 'LINESTRING', 'MULTILINESTRING'))
        RETURN CAST('Geometries in @GeometryCollection must all be IN (''POINT'',''MULTIPOINT'', ''LINESTRING'', ''MULTILINESTRING'')' as GEOMETRY);

    DECLARE @SRID INT = (SELECT DISTINCT Point.STSrid FROM @PointMatrix)

    UPDATE @PointMatrix
    SET LinestringId = NULL
    WHERE GeometryIndex IN (
        SELECT GeometryIndex FROM @PointMatrix WHERE Duplicate = 1
    )

    DELETE @PointMatrix
    WHERE Duplicate = 1;

    -- Data smear
    WITH Cnt AS (
        SELECT PointId, Point, LinestringId,c=COUNT(LinestringId) OVER (ORDER BY PointId)
        FROM @PointMatrix
    ), SmearedLineStringId AS (
        SELECT PointId, Point, LinestringId=MAX(LinestringId) OVER (PARTITION BY c)
        FROM Cnt
    )

    INSERT @Linestrings
    SELECT 
        LinestringId
        ,'(' + 
                STUFF((
                    SELECT ',' + CAST(Point.STX as varchar(100)) + ' ' + CAST(Point.STY as varchar(100))
                    FROM SmearedLineStringId t2
                    WHERE t1.LinestringId = t2.LinestringId 
                    ORDER BY PointId
                    FOR XML PATH ('')
                ), 1, 1, '')
        + ')' as PointArray
    FROM SmearedLineStringId t1
    GROUP BY LinestringId

    DECLARE @Type varchar(100) = CASE 
        WHEN 1 =(SELECT COUNT(*) FROM @PointMatrix) THEN
            'POINT'
        WHEN 1 =(SELECT COUNT(*) FROM @Linestrings) THEN
            'LINESTRING'
        ELSE
            'MULTILINESTRING'
    END

    DECLARE @BeginParens char(1) = '(';
    DECLARE @EndParens char(1) = ')'

    IF @Type != 'MULTILINESTRING'
    BEGIN
        SET @BeginParens = '';
        SET @EndParens = '';
    END

    DECLARE @Wkt varchar(max) = @Type + @BeginParens + 
                STUFF((
                    SELECT ',' + PointArrayStr
                    FROM @Linestrings t2
                    ORDER BY LinestringId
                    FOR XML PATH ('')
                ), 1, 1, '')
            + @EndParens

    RETURN Geometry::STGeomFromText(@Wkt, @SRID)

END
GO

У вас определенно есть полный набор трюков с SQL. Очень круто ;-) Думал добавить толерантность, но времени как-то не хватило.

Clay 22.03.2019 16:08

Другие вопросы по теме