Use case 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;
The WKT for the output is
MULTILINESTRING ((7 8, 3.2 4), (3 4, 1 2))
when I would want
MULTILINESTRING ((1 2, 3 4), (3.2 4, 7 8))
Where the beginning of the "A" and "B" line should be (1 2)
and (3.2 4)
respectfully.
This behavior of UnionAggregate
doesn't seem to care about "direction" of the geometry in order to maintain that A union B and B union A is the same result. However, I want to preserve start/endpoints as I am unioning street geometry and I want all the LINESTRINGs to go in their original direction.
This problem is discussed here: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/89e95366-3649-4294-a0bc-f3921598157f/union-of-linestrings-and-reversing-direction?forum=sqlspatial
They seem to suggest at a possible solution about checking the end result, but it is not clear to me how to do that. It is hinted at in a linked thread that
The MultiLineString always represents the graph from the point which farthest from the origin point.
It is not clear to me exactly what this means, but I don't think I can just assume the result of a UnionAggregate is always the reverse of what I want
If it is hard to know directional intent then I can add M measures where the direction should follow increasing M values.
Assuming I have a method for reversing the points in line, how would I go about solving for this?
I found a function that mimics for STUnion
for added support for Z and M measure: http://www.spatialdbadvisor.com/files/SQLServer.html#robo48 however it is noted that "their direction could change (eg Start/Start Point relationship).", which is what I want to avoid.
Edit:
The functionality I also need is that when to LINESTRING have a shared endpoint, the result is a connect LINESTRING
Use case 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;
This results in WKT LINESTRING (7 8, 3 4, 1 2)
When I would want
LINESTRING (1 2, 3 4, 7 8)
Attempt at solution
The geometry::CollectionAggregate(shape).Reduce(0)
as suggested by Clay solves use case 1. I tried just using STUnion on the result with an empty linestring and while it works it falls back to the incorrect ordering.
I suspect the solution will be a scaler function similar to ST_LineMerge which takes the result of the CollectionAggregate (MULTILINESTRING) and then merges the points together when it can into one LINESTRING, and when it can't returns the geometry back unaltered
Originally, I suggested...
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
You get:
...however, it became clear to me that the answer I gave isn't quite good enough. For example, it's kinda hard to keep Reduce()
from simplifying away part of your lines,
I still like the CollectionAggregate for getting your original array of lines into a single thing, but then I figured there just has to be a way of building the requisite geometry structure.
I played with this several times, and this iteration will eval to a LineString
or a MultiLineString
depending on whether there are disjoint LineString
elements in the inputs:
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
...and finally, given:
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
...you get: