SQL Server 2016 中空间数据的 MakeValid() 替代方法

Cap*_*ock 13 sql-server spatial sql-server-2016

我有一个非常大的地理LINESTRING数据表,我要从 Oracle 移动到 SQL Server。在 Oracle 中有许多针对此数据执行的评估,并且它们也需要针对 SQL Server 中的数据执行。

问题:SQL Server 对有效的要求LINESTRING比 Oracle更严格;“LineString 实例不能在两个或多个连续点的间隔内重叠”。 碰巧我们的LINESTRINGs的百分比不符合该标准,这意味着我们需要评估数据的函数失败。我需要调整数据,以便它可以在 SQL Server 中成功验证。

例如:

验证一个非常简单的LINESTRING自我加倍:

select geography::STGeomFromText(
    'LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326).IsValidDetailed()
Run Code Online (Sandbox Code Playgroud)
24413: Not valid because of two overlapping edges in curve (1).
Run Code Online (Sandbox Code Playgroud)

MakeValid针对它执行函数:

select geography::STGeomFromText(
    'LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326).MakeValid().STAsText()
Run Code Online (Sandbox Code Playgroud)
LINESTRING (0 -0.999999999999867, 0 0, 0 0.999999999999867)
Run Code Online (Sandbox Code Playgroud)

不幸的是,该MakeValid函数更改了点的顺序并删除了第三维,这使我们无法使用它。我正在寻找另一种无需重新排序或删除第三维即可解决此问题的方法。

有任何想法吗?

我的实际数据包含数百/数千个点。

Bra*_*adC 13

让我警告一下,我是第一次在 SQL Server 中使用空间数据(所以你可能已经知道这第一部分),但我花了一段时间才弄清楚SQL Server 没有将 (xyz) 坐标视为真实3D 值,它将它们视为(纬度经度),带有可选的“海拔”值 Z,验证和其他函数会忽略该值。

证据:

select geography::STGeomFromText('LINESTRING (0 0 1, 0 1 2, 0 -1 3)', 4326)
    .IsValidDetailed()

24413: Not valid because of two overlapping edges in curve (1).
Run Code Online (Sandbox Code Playgroud)

你的第一个例子对我来说似乎很奇怪,因为 (0 0 1)、(0 1 2) 和 (0 -1 3)在 3D 空间中不共线(我是一名数学家,所以我在考虑这些方面)。IsValidDetailed(and MakeValid) 将这些视为 (0 0)、(0 1) 和 (0, -1),这确实会产生重叠线。

为了证明这一点,只需交换 X 和 Z,它就会验证:

select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
    .IsValidDetailed()

24400: Valid
Run Code Online (Sandbox Code Playgroud)

如果我们将这些视为在地球表面追踪的区域或路径,而不是数学 3D 空间中的点,这实际上是有道理的。


您问题的第二部分是SQL 不通过函数保留Z(和 M)点值

Z 坐标不用于库进行的任何计算,也不通过任何库计算进行。

不幸的是,这是设计使然。这在 2010 年被报告给微软,请求被关闭为“不会修复”。您可能会发现讨论相关,他们的理由是:

分配 Z 和 M 是不明确的,因为 MakeValid 拆分和合并空间元素。在此过程中,点通常会被创建、删除或移动。因此 MakeValid(和其他结构)会丢弃 Z 和 M 值。

例如:

DECLARE @a geometry = geometry::Parse('POINT(0 0 2 2)');
DECLARE @b geometry = geometry::Parse('POINT(0 0 1 1)');
SELECT @a.STUnion(@b).AsTextZM()
Run Code Online (Sandbox Code Playgroud)

对于点 (0 0),值 Z 和 M 是不明确的。我们决定完全放弃 Z 和 M,而不是返回一半正确的结果。

如果您确切地知道如何分配,您可以稍后分配它们。或者,您可以更改生成对象的方式以在输入时有效,或保留两个版本的对象,一个有效,另一个保留所有功能。如果您更好地解释您的场景以及您对对象的处理方式,也许我们可以为您提供其他解决方法。

此外,正如你已经看到的,MakeValid还可以做其他意想不到的事情,比如改变点的顺序,返回一个 MULTILINESTRING,甚至返回一个 POINT 对象。


我遇到的一个想法是将它们存储为 MULTIPOINT 对象

问题是当您的线串实际回溯之前由线追踪的两点之间的连续线段时。根据定义,如果您要回溯现有点,那么线串不再是可以表示该点集的最简单几何图形,而 MakeValid() 将为您提供多线串(并丢失 Z/M 值)。

不幸的是,如果您正在使用 GPS 数据或类似数据,那么您很可能在路线中的某个点回溯了您的路径,因此线串在这些情况下并不总是那么有用:( 可以说,此类数据应存储为无论如何都是多点,因为您的数据表示在常规时间点采样的对象的离散位置。

在您的情况下,它验证得很好:

select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
    .IsValidDetailed()

24400: Valid
Run Code Online (Sandbox Code Playgroud)

如果您绝对需要将这些保持为 LINESTRINGS,那么您将不得不编写自己的版本,MakeValid通过一些微小的值稍微调整一些源 X 或 Y 点,同时仍然保留 Z(并且不会做其他疯狂的事情,例如将其转换为其他对象类型)。

我仍在编写一些代码,但请在此处查看一些初始想法:


编辑好的,我在测试时发现了一些东西:

  • 如果几何对象无效,您就无能为力了。您无法阅读STGeometryType,您无法获得STNumPoints或 用于STPointN遍历它们。如果您不能使用MakeValid,则您基本上只能对地理对象的文本表示进行操作。
  • 使用STAsText()将返回甚至无效对象的文本表示,但不返回 Z 或 M 值。相反,我们想要AsTextZM()or ToString()
  • 你不能创建一个调用的函数RAND()(函数需要是确定性的),所以我只是用越来越大的值来推动它。我真的不知道您的数据的精度是多少,也不知道它对微小变化的容忍度如何,因此请自行决定使用或修改此功能。

我不知道是否有可能的输入会导致这个循环永远持续下去。你被警告了。

CREATE FUNCTION dbo.FixBadLineString (@input geography) RETURNS geography
AS BEGIN
DECLARE @output geography

IF @input.STIsValid() = 1   --send valid objects back as-is
  SET @output = @input;
ELSE IF LEFT(@input.IsValidDetailed(),6) = '24413:'
--"Not valid because of two overlapping edges in curve"
BEGIN
  --make a new MultiPoint object from the LineString text
  DECLARE @mp geography = geography::STGeomFromText(
      REPLACE(@input.AsTextZM(), 'LINESTRING', 'MULTIPOINT'), 4326);
  DECLARE @newText nvarchar(max); --to build output
  DECLARE @point int 
  DECLARE @tinynum float = 0;

  SET @output = @input;
  --keep going until it validates
  WHILE @output.STIsValid() = 0
  BEGIN
    SET @newText = 'LINESTRING (';
    SET @point = 1
    SET @tinynum = @tinynum + 0.00000001

    --Loop through the points, add a bit and append to the new string
    WHILE @point <= @mp.STNumPoints()
    BEGIN
      SET @newText = @newText + convert(varchar(50),
               @mp.STPointN(@point).Long + @tinynum) + ' ';
      SET @newText = @newText + convert(varchar(50),
               @mp.STPointN(@point).Lat - @tinynum) + ' ';
      SET @newText = @newText + convert(varchar(50), 
               @mp.STPointN(@point).Z) + ', ';
      SET @tinynum = @tinynum * -2
      SET @point = @point + 1
    END

    --close the parens and make the new LineString object
    SET @newText = LEFT(@newText, LEN(@newText) - 1) + ')'
    SET @output = geography::STGeomFromText(@newText, 4326);
  END; --this will loop if it is still invalid
  RETURN @output;
END;
--Any other unhandled error, just send back NULL
ELSE SET @output = NULL;

RETURN @output;
END
Run Code Online (Sandbox Code Playgroud)

我没有解析字符串,而是选择MultiPoint使用相同的一组点创建一个新对象,这样我就可以遍历它们并轻推它们,然后重新组合一个新的 LineString。这是一些测试代码,其中 3 个值(包括您的示例)开始无效但已修复:

declare @geostuff table (baddata geography)

INSERT INTO @geostuff (baddata)
          SELECT geography::STGeomFromText('LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 2 0, 0 1 0.5, 0 -1 -14)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 0 4, 1 1 40, -1 -1 23)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (1 1 9, 0 1 -.5, 0 -1 3)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (6 6 26.5, 4 4 42, 12 12 86)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 0 2, -4 4 -2, 4 -4 0)',4326)

SELECT baddata.AsTextZM() as before, baddata.IsValidDetailed() as pretest,
 dbo.FixBadLineString(baddata).AsTextZM() as after,
 dbo.FixBadLineString(baddata).IsValidDetailed() as posttest 
FROM @geostuff
Run Code Online (Sandbox Code Playgroud)