Search code examples
sql-servert-sqlgeospatialsqlgeographysqlgeometry

Group by value and create geography polyline from points (latitude and longitude) for each group in T-SQL


A similar question has been asked here:

Create geography polyline from points in T-SQL

Taking that question further, I have a table schema that looks like this:

CREATE TABLE [dbo].[LongAndLats](
[Longitude] [float] NULL,
[Latitude] [float] NULL,
[SortOrder] [bigint] NULL,
[SensorID] [bigint] NULL,
)

Sample data looks like this:

enter image description here

How can I convert these points into a geography polyline for each SensorID using TSQL (so that I would have a SensorID/Polyline record for each SensorID)?

I've tried using a db_cursor but I get a separate result set for each group (and I think the geographies might be the same). This code:

DECLARE @SensorID VARCHAR(2000)
DECLARE @LineFromPoints geography
DECLARE @BuildString NVARCHAR(MAX)

DECLARE db_cursor CURSOR FOR  
SELECT Distinct([SensorId]) 
FROM [dbo].[LongAndLats]

OPEN db_cursor   
FETCH NEXT FROM db_cursor INTO LongAndLats 

WHILE @@FETCH_STATUS = 0   
BEGIN   
       SELECT @BuildString = COALESCE(@BuildString + ',', '') + CAST([Longitude] AS NVARCHAR(50)) + ' ' + CAST([Latitude] AS NVARCHAR(50))
       FROM [LongAndLats]
       WHERE SensorID = @SensorID
       ORDER BY SortOrder            

       SET @BuildString = 'LINESTRING(' + @BuildString + ')';   
       SET @LineFromPoints = geography::STLineFromText(@BuildString, 4326);
       SELECT @LineFromPoints As 'Geomerty', @name As 'SensorID' 

       FETCH NEXT FROM db_cursor INTO @name   
END   

CLOSE db_cursor   
DEALLOCATE db_cursor

Results in this:

enter image description here

Ultimately, I'd like to have a view returning all of the SensorID/Polyline pairs. I don't know that my current approach is going to work. I would appreciate any suggestions or examples.


Solution

  • From SQL Server 2017+ you could use:

    SELECT geography::STLineFromText('LINESTRING(' + 
             STRING_AGG(CONCAT(Longitude, ' ' ,Latitude), ',') 
             WITHIN GROUP(ORDER BY SortOrder) + ')' , 4326) AS geometry
          ,SensorId
    FROM dbo.LongAndLats
    GROUP BY SensorId
    HAVING COUNT(*) > 1;
    

    DBFiddle Demo


    I've tried using a db_cursor but I get a separate result set for each group

    Please avoid cursors, end each line with semicolon and stop using:

    SELECT @BuildString = COALESCE(@BuildString + ',', '') 
           + CAST([Longitude] AS NVARCHAR(50)) + ' ' + CAST([Latitude] 
            AS NVARCHAR(50))
    FROM [LongAndLats]
    WHERE SensorID = @SensorID
    ORDER BY SortOrder;  
    

    Construct above may look ok, but it could lead to undefined behaviour. More info: nvarchar concatenation / index / nvarchar(max) inexplicable behavior

    EDIT:

    SQL Server 2012 version:

    SELECT geography::STLineFromText('LINESTRING(' 
          + STUFF(
                 (SELECT ',' + CONCAT(Longitude, ' ' ,Latitude) 
                  FROM dbo.LongAndLats t2
                  WHERE t1.SensorId = t2.SensorId 
                  ORDER BY SortOrder
                  FOR XML PATH (''))
                 , 1, 1, '')
           + ')' 
           , 4326) AS geometry, SensorId
    FROM dbo.LongAndLats t1
    GROUP BY SensorId
    HAVING COUNT(*) > 1;
    

    DBFiddle Demo2

    EDIT2:

    To avoid:

    A .NET Framework error occurred during execution of user-defined routine or aggregate "geography":

    System.FormatException: 24117: The LineString input is not valid because it does not have enough points. A LineString must have at least two points.

    you could add HAVING COUNT(*) > 1;

    FINAL EDIT:

    If you have "garbage data", just filter it out(or add CHECK constraint on that column):

    "Latitude values must be between -90 and 90 degrees"

    SELECT geography::STLineFromText('LINESTRING(' 
          + STUFF(
                 (SELECT ',' + CONCAT(Longitude, ' ' ,Latitude) 
                  FROM dbo.LongAndLats t2
                  WHERE t1.SensorId = t2.SensorId 
                    AND Latitude BETWEEN -90 and 90
                    AND Longitude BETWEEN -180 AND 180
                  ORDER BY SortOrder
                  FOR XML PATH (''))
                 , 1, 1, '')
           + ')' 
           , 4326) AS geometry, SensorId
    FROM dbo.LongAndLats t1
    WHERE Latitude BETWEEN -90 and 90
      AND Longitude BETWEEN -180 AND 180
    GROUP BY SensorId
    HAVING COUNT(*) > 1;
    

    DBFiddle Demo3