4

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.

user3799279
  • 143
  • 1
  • 11

1 Answers1

8

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

Lukasz Szozda
  • 162,964
  • 23
  • 234
  • 275
  • Thanks. I should have added I'm using SQL Server 2012. What do you think of this example (https://gooroo.io/GoorooTHINK/Article/10001/Aggregate-String-Concatenation-in-SQL-Server-2012-like-stringagg-in-PostgreSql/5122#.WlowN6inFPY) as a STRING_AGG equivalent? – user3799279 Jan 13 '18 at 16:21
  • @user3799279 Please check STUFF + XML version – Lukasz Szozda Jan 13 '18 at 16:35
  • Thanks again. I've gotten a FormatException "Latitude values must be between -90 and 90 degrees" and can filter those out in the whereclause but then get another FormatException "The LineString input is not valid because it does not have enough points. A LineString must have at least two points." How might I filter on Count([SensorId]) > 2? – user3799279 Jan 13 '18 at 17:08
  • @user3799279 `HAVING COUNT(*) > 1;` – Lukasz Szozda Jan 13 '18 at 17:30
  • I've tried that and get the same error. If I comment out the geometry field portion of the select, I do get back the list of SensorID's. The only difference between what I've tried and your example is my whereclause is "WHERE t1.SensorId = t2.SensorId AND (Latitude <= 90)" – user3799279 Jan 13 '18 at 18:06
  • @user3799279 This is my final edit. It is very easy to add `WHERE` condtion. You could accept this answer or not. – Lukasz Szozda Jan 13 '18 at 18:15
  • Thank you for all your help. I have accepted your answer, your detail is excellent. For whatever reason with my data, and for reasons I don't yet understand, the where clause filtering out the garbage data still throws a FormatException. If I create a view and filter the garbage out there (with the same where clause in your example) and then apply your example to the view, it works perfectly. – user3799279 Jan 13 '18 at 21:06
  • @user3799279 Wild guess, (your column has **float** datatype). Maybe the problem is scientific notation like `LINESTRING(1e-14 1, 2 2)`. I mean values that are so small that they are represented in alternative form. You could try to change datatype to `DECIMAL(11,7)` or do some rounding `ROUND(col_name, -6)` – Lukasz Szozda Jan 14 '18 at 08:27
  • I believe the issue might be scientific notation but I notice in your fiddle the issue persists there as well. If you change the first set of values from (1,1,1,10) to (1,91,1,10) and run it, you'll get the same FormatException. Should the lat/long validation not be moved to the whereclause where the lat/long concatenation is happening? WHERE t1.SensorId = t2.SensorId AND Latitude BETWEEN -90 and 90 AND Longitude BETWEEN -180 AND 180? – user3799279 Jan 16 '18 at 14:46
  • @user3799279 Please check: http://dbfiddle.uk/?rdbms=sqlserver_2014&fiddle=23785ffab141fa0236e2b03af60353b7 I've added the WHERE clause in main and subquery and now there is no error. – Lukasz Szozda Jan 16 '18 at 15:34
  • 1
    That is slick, nice work. I appreciate your time and examples. – user3799279 Jan 16 '18 at 16:37